Compare commits

...
Author SHA1 Message Date
arkon 15a3b3996b chore: version packages 2026-03-05 00:28:13 +01:00
arkonandSigurður Guðbrandsson 1b76e6e2e2 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>
2026-03-05 00:27:07 +01:00
4 changed files with 48 additions and 8 deletions
+6
View File
@@ -1,5 +1,11 @@
# aicodeman
## 0.3.4
### Patch Changes
- Fix Chrome tab freeze from flicker filter buffer accumulation during active sessions, and fix shell mode feedback delay by excluding shell sessions from cursor-up filter
## 0.3.3
### Patch Changes
+1 -1
View File
@@ -52,7 +52,7 @@ When user says "COM":
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
**Version**: 0.3.3 (must match `package.json`)
**Version**: 0.3.4 (must match `package.json`)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aicodeman",
"version": "0.3.3",
"version": "0.3.4",
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+40 -6
View File
@@ -1097,18 +1097,47 @@ class CodemanApp {
// 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);
//
// Shell mode is excluded: shell readline also uses cursor-up for prompt redraws
// (e.g. zsh syntax highlighting on every keystroke), and there's no Ink status bar
// to protect. Applying the filter in shell mode delays character feedback until the
// user stops typing for 50ms, making the terminal feel unresponsive.
const isShellMode = session?.mode === 'shell';
const hasCursorUpRedraw = !isShellMode && /\x1b\[\d{1,2}A/.test(data);
if (hasCursorUpRedraw || (this.flickerFilterActive && !flickerFilterEnabled)) {
this.flickerFilterActive = true;
this.flickerFilterBuffer += data;
if (this.flickerFilterTimeout) {
clearTimeout(this.flickerFilterTimeout);
// Only reset the 50ms timer on cursor-up events (start of a new Ink redraw cycle).
// Non-cursor-up events while the filter is active are trailing data from the same
// redraw — don't extend the deadline further. Without this guard, a busy Claude
// session emitting terminal data faster than SYNC_WAIT_TIMEOUT_MS never flushes,
// accumulating MBs in flickerFilterBuffer that freeze Chrome all at once.
if (hasCursorUpRedraw) {
if (this.flickerFilterTimeout) {
clearTimeout(this.flickerFilterTimeout);
}
this.flickerFilterTimeout = setTimeout(() => {
this.flickerFilterTimeout = null;
this.flushFlickerBuffer();
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
} else if (!this.flickerFilterTimeout) {
// Safety: if no timer is running for some reason, ensure we eventually flush.
this.flickerFilterTimeout = setTimeout(() => {
this.flickerFilterTimeout = null;
this.flushFlickerBuffer();
}, SYNC_WAIT_TIMEOUT_MS);
}
this.flickerFilterTimeout = setTimeout(() => {
this.flickerFilterTimeout = null;
// Safety valve: if buffer grew very large (e.g. from a burst before the timer fired),
// flush immediately to avoid writing a huge block all at once.
if (this.flickerFilterBuffer.length > 256 * 1024) {
if (this.flickerFilterTimeout) {
clearTimeout(this.flickerFilterTimeout);
this.flickerFilterTimeout = null;
}
this.flushFlickerBuffer();
}, SYNC_WAIT_TIMEOUT_MS); // 50ms buffer window
}
return;
}
@@ -1238,6 +1267,11 @@ class CodemanApp {
} catch { return null; }
}
});
} else if (session.mode === 'shell') {
// Shell mode: the shell provides its own PTY echo so the overlay isn't needed.
// Disable it by clearing any pending text.
this._localEchoOverlay.clear();
this._localEchoEnabled = false;
} else {
// Claude Code: scan for ❯ prompt character
this._localEchoOverlay.setPrompt({ type: 'character', char: '\u276f', offset: 2 });