Skip to content

Commit 1b76e6e

Browse files
Ark0NSGudbrandsson
andcommitted
fix: prevent Chrome freeze and shell feedback delay from flicker filter
Bug 1: Every incoming SSE terminal event reset the 50ms flush timer, not just cursor-up events. During active Claude runs the timer never fired, accumulating MBs in flickerFilterBuffer that froze Chrome on flush. Fix: only reset timer on cursor-up events; add 256KB safety valve. Bug 2: Shell sessions emit cursor-up on every keystroke for readline prompt redraws, triggering the flicker filter and delaying feedback. Fix: skip cursor-up filter for shell mode; disable local echo overlay. Based on PR #31 by @SGudbrandsson. Co-Authored-By: Sigurður Guðbrandsson <SGudbrandsson@users.noreply.github.com>
1 parent f8b81b8 commit 1b76e6e

File tree

1 file changed

+40
-6
lines changed

1 file changed

+40
-6
lines changed

src/web/public/app.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,18 +1097,47 @@ class CodemanApp {
10971097
// Ink's status bar updates use cursor-up + erase-line + rewrite, which can split
10981098
// across render frames causing old/new status text to overlap (garbled output).
10991099
// Buffering for 50ms ensures the full redraw arrives atomically.
1100-
const hasCursorUpRedraw = /\x1b\[\d{1,2}A/.test(data);
1100+
//
1101+
// Shell mode is excluded: shell readline also uses cursor-up for prompt redraws
1102+
// (e.g. zsh syntax highlighting on every keystroke), and there's no Ink status bar
1103+
// to protect. Applying the filter in shell mode delays character feedback until the
1104+
// user stops typing for 50ms, making the terminal feel unresponsive.
1105+
const isShellMode = session?.mode === 'shell';
1106+
const hasCursorUpRedraw = !isShellMode && /\x1b\[\d{1,2}A/.test(data);
11011107
if (hasCursorUpRedraw || (this.flickerFilterActive && !flickerFilterEnabled)) {
11021108
this.flickerFilterActive = true;
11031109
this.flickerFilterBuffer += data;
11041110

1105-
if (this.flickerFilterTimeout) {
1106-
clearTimeout(this.flickerFilterTimeout);
1111+
// Only reset the 50ms timer on cursor-up events (start of a new Ink redraw cycle).
1112+
// Non-cursor-up events while the filter is active are trailing data from the same
1113+
// redraw — don't extend the deadline further. Without this guard, a busy Claude
1114+
// session emitting terminal data faster than SYNC_WAIT_TIMEOUT_MS never flushes,
1115+
// accumulating MBs in flickerFilterBuffer that freeze Chrome all at once.
1116+
if (hasCursorUpRedraw) {
1117+
if (this.flickerFilterTimeout) {
1118+
clearTimeout(this.flickerFilterTimeout);
1119+
}
1120+
this.flickerFilterTimeout = setTimeout(() => {
1121+
this.flickerFilterTimeout = null;
1122+
this.flushFlickerBuffer();
1123+
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
1124+
} else if (!this.flickerFilterTimeout) {
1125+
// Safety: if no timer is running for some reason, ensure we eventually flush.
1126+
this.flickerFilterTimeout = setTimeout(() => {
1127+
this.flickerFilterTimeout = null;
1128+
this.flushFlickerBuffer();
1129+
}, SYNC_WAIT_TIMEOUT_MS);
11071130
}
1108-
this.flickerFilterTimeout = setTimeout(() => {
1109-
this.flickerFilterTimeout = null;
1131+
1132+
// Safety valve: if buffer grew very large (e.g. from a burst before the timer fired),
1133+
// flush immediately to avoid writing a huge block all at once.
1134+
if (this.flickerFilterBuffer.length > 256 * 1024) {
1135+
if (this.flickerFilterTimeout) {
1136+
clearTimeout(this.flickerFilterTimeout);
1137+
this.flickerFilterTimeout = null;
1138+
}
11101139
this.flushFlickerBuffer();
1111-
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
1140+
}
11121141

11131142
return;
11141143
}
@@ -1238,6 +1267,11 @@ class CodemanApp {
12381267
} catch { return null; }
12391268
}
12401269
});
1270+
} else if (session.mode === 'shell') {
1271+
// Shell mode: the shell provides its own PTY echo so the overlay isn't needed.
1272+
// Disable it by clearing any pending text.
1273+
this._localEchoOverlay.clear();
1274+
this._localEchoEnabled = false;
12411275
} else {
12421276
// Claude Code: scan for ❯ prompt character
12431277
this._localEchoOverlay.setPrompt({ type: 'character', char: '\u276f', offset: 2 });

0 commit comments

Comments
 (0)