mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
feat: local echo multi-line wrapping — long input wraps to next rows matching terminal character-wrap behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ When user says "COM":
|
|||||||
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
|
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
|
||||||
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
||||||
|
|
||||||
**Version**: 0.1552 (must match `package.json` for npm publish)
|
**Version**: 0.1553 (must match `package.json` for npm publish)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1552",
|
"version": "0.1553",
|
||||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+85
-34
@@ -846,28 +846,23 @@ class FocusTrap {
|
|||||||
class LocalEchoOverlay {
|
class LocalEchoOverlay {
|
||||||
constructor(terminal) {
|
constructor(terminal) {
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
this.overlay = document.createElement('span');
|
// Container: positioned at prompt row, holds per-line child divs
|
||||||
this.overlay.style.cssText = 'position:absolute;z-index:7;pointer-events:none;white-space:pre;font-kerning:none;overflow:hidden;display:none';
|
this.overlay = document.createElement('div');
|
||||||
this.overlay.style.fontFamily = terminal.options.fontFamily;
|
this.overlay.style.cssText = 'position:absolute;z-index:7;pointer-events:none;display:none';
|
||||||
this.overlay.style.fontSize = terminal.options.fontSize + 'px';
|
|
||||||
this.overlay.style.fontWeight = terminal.options.fontWeight || 'normal';
|
|
||||||
// Match letter-spacing and colors from xterm-rows for DPR compensation
|
|
||||||
const screen = terminal.element.querySelector('.xterm-screen');
|
const screen = terminal.element.querySelector('.xterm-screen');
|
||||||
|
// Cache font/color styles for line divs
|
||||||
|
this._fontFamily = terminal.options.fontFamily;
|
||||||
|
this._fontSize = terminal.options.fontSize + 'px';
|
||||||
|
this._fontWeight = terminal.options.fontWeight || 'normal';
|
||||||
|
this._bg = terminal.options?.theme?.background || '#0d0d0d';
|
||||||
|
this._color = '';
|
||||||
|
this._letterSpacing = '';
|
||||||
const rows = terminal.element.querySelector('.xterm-rows');
|
const rows = terminal.element.querySelector('.xterm-rows');
|
||||||
if (rows) {
|
if (rows) {
|
||||||
this.overlay.style.letterSpacing = getComputedStyle(rows).letterSpacing;
|
const cs = getComputedStyle(rows);
|
||||||
this.overlay.style.color = getComputedStyle(rows).color;
|
this._letterSpacing = cs.letterSpacing;
|
||||||
|
this._color = cs.color;
|
||||||
}
|
}
|
||||||
// Opaque background so overlay covers old buffer text underneath.
|
|
||||||
// xterm.js applies bg via internal rendering, not CSS — use theme option.
|
|
||||||
this.overlay.style.backgroundColor = terminal.options?.theme?.background || '#0d0d0d';
|
|
||||||
// Persistent text node so _cursor span survives _render() updates
|
|
||||||
this._textNode = document.createTextNode('');
|
|
||||||
this.overlay.appendChild(this._textNode);
|
|
||||||
// Block cursor matching Claude Code's idle prompt cursor
|
|
||||||
this._cursor = document.createElement('span');
|
|
||||||
this._cursor.style.cssText = 'display:inline-block;vertical-align:top';
|
|
||||||
this.overlay.appendChild(this._cursor);
|
|
||||||
screen.appendChild(this.overlay);
|
screen.appendChild(this.overlay);
|
||||||
this.pendingText = '';
|
this.pendingText = '';
|
||||||
this._storageKey = 'claudeman_local_echo_pending';
|
this._storageKey = 'claudeman_local_echo_pending';
|
||||||
@@ -930,10 +925,30 @@ class LocalEchoOverlay {
|
|||||||
clear() {
|
clear() {
|
||||||
this.pendingText = '';
|
this.pendingText = '';
|
||||||
this._persist();
|
this._persist();
|
||||||
this._textNode.textContent = '';
|
this._lastRenderKey = '';
|
||||||
|
this.overlay.innerHTML = '';
|
||||||
this.overlay.style.display = 'none';
|
this.overlay.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a styled line div with opaque background
|
||||||
|
_makeLine(text, leftPx, topPx, widthPx, cellH) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = `position:absolute;white-space:pre;font-kerning:none;overflow:hidden;pointer-events:none`;
|
||||||
|
el.style.fontFamily = this._fontFamily;
|
||||||
|
el.style.fontSize = this._fontSize;
|
||||||
|
el.style.fontWeight = this._fontWeight;
|
||||||
|
el.style.letterSpacing = this._letterSpacing;
|
||||||
|
el.style.color = this._color;
|
||||||
|
el.style.backgroundColor = this._bg;
|
||||||
|
el.style.left = leftPx + 'px';
|
||||||
|
el.style.top = topPx + 'px';
|
||||||
|
el.style.width = widthPx + 'px';
|
||||||
|
el.style.height = cellH + 'px';
|
||||||
|
el.style.lineHeight = cellH + 'px';
|
||||||
|
el.textContent = text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
_render() {
|
_render() {
|
||||||
if (!this.pendingText) {
|
if (!this.pendingText) {
|
||||||
this.overlay.style.display = 'none';
|
this.overlay.style.display = 'none';
|
||||||
@@ -947,23 +962,59 @@ class LocalEchoOverlay {
|
|||||||
const dims = this.terminal._core._renderService.dimensions;
|
const dims = this.terminal._core._renderService.dimensions;
|
||||||
const cellW = dims.css.cell.width;
|
const cellW = dims.css.cell.width;
|
||||||
const cellH = dims.css.cell.height;
|
const cellH = dims.css.cell.height;
|
||||||
|
const totalCols = this.terminal.cols;
|
||||||
// Position right after "❯ " (prompt col + 2)
|
// Position right after "❯ " (prompt col + 2)
|
||||||
const col = prompt.col + 2;
|
const startCol = Math.min(prompt.col + 2, totalCols - 1);
|
||||||
const leftPx = col * cellW;
|
const firstLineCols = Math.max(1, totalCols - startCol);
|
||||||
this.overlay.style.left = leftPx + 'px';
|
|
||||||
|
// Skip redundant re-renders (e.g. from flushPendingWrites at 60fps)
|
||||||
|
const renderKey = `${this.pendingText.length}:${prompt.row}:${prompt.col}:${totalCols}`;
|
||||||
|
if (renderKey === this._lastRenderKey && this.overlay.style.display !== 'none') return;
|
||||||
|
this._lastRenderKey = renderKey;
|
||||||
|
|
||||||
|
// Split text into visual lines matching terminal character-wrap behavior
|
||||||
|
// Line 0: starts after prompt, fits (totalCols - startCol) chars
|
||||||
|
// Line 1+: starts at col 0, fits totalCols chars each
|
||||||
|
const lines = [];
|
||||||
|
let remaining = this.pendingText;
|
||||||
|
lines.push(remaining.slice(0, firstLineCols));
|
||||||
|
remaining = remaining.slice(firstLineCols);
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
lines.push(remaining.slice(0, totalCols));
|
||||||
|
remaining = remaining.slice(totalCols);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position overlay container at prompt row
|
||||||
|
this.overlay.style.left = '0px';
|
||||||
this.overlay.style.top = (prompt.row * cellH) + 'px';
|
this.overlay.style.top = (prompt.row * cellH) + 'px';
|
||||||
this.overlay.style.height = cellH + 'px';
|
// Cover any subpixel seam above first line
|
||||||
this.overlay.style.lineHeight = cellH + 'px';
|
this.overlay.style.boxShadow = `0 -1px 0 0 ${this._bg}`;
|
||||||
// Extend to right edge to cover old buffer text with opaque bg
|
|
||||||
this.overlay.style.width = (this.terminal.cols * cellW - leftPx) + 'px';
|
// Build line elements — recreate on each render (typically 1-3 divs, negligible cost)
|
||||||
// Cover any subpixel seam above overlay with matching box-shadow
|
this.overlay.innerHTML = '';
|
||||||
const bg = this.overlay.style.backgroundColor;
|
const fullWidthPx = totalCols * cellW;
|
||||||
this.overlay.style.boxShadow = `0 -1px 0 0 ${bg}`;
|
for (let i = 0; i < lines.length; i++) {
|
||||||
this._textNode.textContent = this.pendingText;
|
const leftPx = i === 0 ? startCol * cellW : 0;
|
||||||
// Block cursor: full cell width & height, matching Claude Code's cursor color
|
const widthPx = i === 0 ? (fullWidthPx - leftPx) : fullWidthPx;
|
||||||
this._cursor.style.width = cellW + 'px';
|
const topPx = i * cellH;
|
||||||
this._cursor.style.height = cellH + 'px';
|
this.overlay.appendChild(this._makeLine(lines[i], leftPx, topPx, widthPx, cellH));
|
||||||
this._cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0';
|
}
|
||||||
|
|
||||||
|
// Block cursor at end of last line
|
||||||
|
const lastLine = lines[lines.length - 1];
|
||||||
|
const lastLineLeft = lines.length === 1 ? startCol : 0;
|
||||||
|
const cursorCol = lastLineLeft + lastLine.length;
|
||||||
|
if (cursorCol < totalCols) {
|
||||||
|
const cursor = document.createElement('span');
|
||||||
|
cursor.style.cssText = 'position:absolute;display:inline-block';
|
||||||
|
cursor.style.left = (cursorCol * cellW) + 'px';
|
||||||
|
cursor.style.top = ((lines.length - 1) * cellH) + 'px';
|
||||||
|
cursor.style.width = cellW + 'px';
|
||||||
|
cursor.style.height = cellH + 'px';
|
||||||
|
cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0';
|
||||||
|
this.overlay.appendChild(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
this.overlay.style.display = '';
|
this.overlay.style.display = '';
|
||||||
} catch {
|
} catch {
|
||||||
this.clear();
|
this.clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user