mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
fix: Ink status bar redraw garbling — never discard incomplete DEC sync blocks, always buffer cursor-up redraws
chore: bump version to 0.1548 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.1547 (must match `package.json` for npm publish)
|
**Version**: 0.1548 (must match `package.json` for npm publish)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1547",
|
"version": "0.1548",
|
||||||
"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",
|
||||||
|
|||||||
+38
-24
@@ -861,9 +861,13 @@ class LocalEchoOverlay {
|
|||||||
// Opaque background so overlay covers old buffer text underneath.
|
// Opaque background so overlay covers old buffer text underneath.
|
||||||
// xterm.js applies bg via internal rendering, not CSS — use theme option.
|
// xterm.js applies bg via internal rendering, not CSS — use theme option.
|
||||||
this.overlay.style.backgroundColor = terminal.options?.theme?.background || '#0d0d0d';
|
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
|
// Block cursor matching Claude Code's idle prompt cursor
|
||||||
this._cursor = document.createElement('span');
|
this._cursor = document.createElement('span');
|
||||||
this._cursor.style.cssText = 'display:inline-block;vertical-align:top';
|
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';
|
||||||
@@ -926,10 +930,7 @@ class LocalEchoOverlay {
|
|||||||
clear() {
|
clear() {
|
||||||
this.pendingText = '';
|
this.pendingText = '';
|
||||||
this._persist();
|
this._persist();
|
||||||
this.overlay.textContent = '';
|
this._textNode.textContent = '';
|
||||||
if (this._cursor.parentNode === this.overlay) {
|
|
||||||
this.overlay.removeChild(this._cursor);
|
|
||||||
}
|
|
||||||
this.overlay.style.display = 'none';
|
this.overlay.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,12 +959,11 @@ class LocalEchoOverlay {
|
|||||||
// Cover any subpixel seam above overlay with matching box-shadow
|
// Cover any subpixel seam above overlay with matching box-shadow
|
||||||
const bg = this.overlay.style.backgroundColor;
|
const bg = this.overlay.style.backgroundColor;
|
||||||
this.overlay.style.boxShadow = `0 -1px 0 0 ${bg}`;
|
this.overlay.style.boxShadow = `0 -1px 0 0 ${bg}`;
|
||||||
this.overlay.textContent = this.pendingText;
|
this._textNode.textContent = this.pendingText;
|
||||||
// Block cursor: full cell width & height, matching Claude Code's cursor color
|
// Block cursor: full cell width & height, matching Claude Code's cursor color
|
||||||
this._cursor.style.width = cellW + 'px';
|
this._cursor.style.width = cellW + 'px';
|
||||||
this._cursor.style.height = cellH + 'px';
|
this._cursor.style.height = cellH + 'px';
|
||||||
this._cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0';
|
this._cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0';
|
||||||
this.overlay.appendChild(this._cursor);
|
|
||||||
this.overlay.style.display = '';
|
this.overlay.style.display = '';
|
||||||
} catch {
|
} catch {
|
||||||
this.clear();
|
this.clear();
|
||||||
@@ -2183,27 +2183,36 @@ class ClaudemanApp {
|
|||||||
const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null;
|
||||||
const flickerFilterEnabled = session?.flickerFilterEnabled ?? false;
|
const flickerFilterEnabled = session?.flickerFilterEnabled ?? false;
|
||||||
|
|
||||||
// Flicker filter: detect screen clear and cursor-up patterns and buffer output
|
// Always buffer Ink's cursor-up redraws regardless of flicker filter setting.
|
||||||
|
// Ink's status bar updates use cursor-up + erase-line + rewrite, which can split
|
||||||
|
// across render frames causing old/new status text to overlap (garbled output).
|
||||||
|
// Buffering for 50ms ensures the full redraw arrives atomically.
|
||||||
|
const hasCursorUpRedraw = /\x1b\[\d{1,2}A/.test(data);
|
||||||
|
if (hasCursorUpRedraw || (this.flickerFilterActive && !flickerFilterEnabled)) {
|
||||||
|
this.flickerFilterActive = true;
|
||||||
|
this.flickerFilterBuffer += data;
|
||||||
|
|
||||||
|
if (this.flickerFilterTimeout) {
|
||||||
|
clearTimeout(this.flickerFilterTimeout);
|
||||||
|
}
|
||||||
|
this.flickerFilterTimeout = setTimeout(() => {
|
||||||
|
this.flickerFilterTimeout = null;
|
||||||
|
this.flushFlickerBuffer();
|
||||||
|
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opt-in flicker filter: also buffer screen clear patterns
|
||||||
if (flickerFilterEnabled) {
|
if (flickerFilterEnabled) {
|
||||||
// Detect Ink's redraw patterns:
|
|
||||||
// - ESC[2J (clear entire screen)
|
|
||||||
// - ESC[H ESC[J (cursor home + clear to end)
|
|
||||||
// - ESC[?25l ESC[H (hide cursor + home - common Ink pattern)
|
|
||||||
// - ESC[nA (cursor up n lines - Ink's line redraw pattern)
|
|
||||||
const hasScreenClear = data.includes('\x1b[2J') ||
|
const hasScreenClear = data.includes('\x1b[2J') ||
|
||||||
data.includes('\x1b[H\x1b[J') ||
|
data.includes('\x1b[H\x1b[J') ||
|
||||||
(data.includes('\x1b[H') && data.includes('\x1b[?25l'));
|
(data.includes('\x1b[H') && data.includes('\x1b[?25l'));
|
||||||
|
|
||||||
// Detect cursor-up patterns (ESC[nA where n >= 2) - indicates Ink redrawing multiple lines
|
if (hasScreenClear) {
|
||||||
// Use regex to find ESC[nA where n is 2 or more digits
|
|
||||||
const hasCursorUpRedraw = /\x1b\[\d{1,2}A/.test(data);
|
|
||||||
|
|
||||||
if (hasScreenClear || hasCursorUpRedraw) {
|
|
||||||
// Redraw pattern detected - activate flicker filter
|
|
||||||
this.flickerFilterActive = true;
|
this.flickerFilterActive = true;
|
||||||
this.flickerFilterBuffer += data;
|
this.flickerFilterBuffer += data;
|
||||||
|
|
||||||
// Clear any existing timeout and set a new one
|
|
||||||
if (this.flickerFilterTimeout) {
|
if (this.flickerFilterTimeout) {
|
||||||
clearTimeout(this.flickerFilterTimeout);
|
clearTimeout(this.flickerFilterTimeout);
|
||||||
}
|
}
|
||||||
@@ -2212,13 +2221,12 @@ class ClaudemanApp {
|
|||||||
this.flushFlickerBuffer();
|
this.flushFlickerBuffer();
|
||||||
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
|
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
|
||||||
|
|
||||||
return; // Don't process normally, wait for buffer window
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.flickerFilterActive) {
|
if (this.flickerFilterActive) {
|
||||||
// Filter is active, accumulate data
|
|
||||||
this.flickerFilterBuffer += data;
|
this.flickerFilterBuffer += data;
|
||||||
return; // Continue buffering
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2313,9 +2321,15 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
// Write all segments in a single batch (atomic within this frame)
|
// Write all segments in a single batch (atomic within this frame)
|
||||||
// xterm.js internally batches multiple write() calls within same frame
|
// xterm.js internally batches multiple write() calls within same frame
|
||||||
|
// Never discard content from incomplete sync blocks — xterm.js doesn't support
|
||||||
|
// DEC 2026 natively anyway, so strip the marker and write content regardless.
|
||||||
|
// Discarding causes real data loss (including Ink's erase-line escapes).
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
if (segment && !segment.startsWith(DEC_SYNC_START)) {
|
if (segment) {
|
||||||
this.terminal.write(segment);
|
const content = segment.startsWith(DEC_SYNC_START)
|
||||||
|
? segment.slice(DEC_SYNC_START.length)
|
||||||
|
: segment;
|
||||||
|
if (content) this.terminal.write(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user