fix: prevent duplicate terminal output on tab switch to busy sessions

Two fixes for the tab-switching corruption bug:

1. _finishBufferLoad() now discards queued SSE events instead of flushing
   them. The loaded API buffer is the source of truth — queued events
   overlap with it, and flushing them writes duplicate Ink cursor-up
   redraws that corrupt the terminal display (garbled text, wrong cursor
   positions).

2. Skip stale cache write for busy sessions. When a session is actively
   working, the cache is always outdated — writing it first and then
   rewriting with the fresh API buffer caused a jarring double-render
   flash. Now busy sessions get a single clean clear+write transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-23 12:32:33 +01:00
co-authored by Claude Opus 4.6
parent a9d83ec4e3
commit 0d80524f10
2 changed files with 22 additions and 14 deletions
+15 -7
View File
@@ -2153,11 +2153,14 @@ class CodemanApp {
// canvas may be at stale dimensions — content would render at wrong width.
if (this.fitAddon) this.fitAddon.fit();
// Instant cache restore — show previous buffer via chunked write to avoid WebGL GPU stalls.
// Direct terminal.write() of large cached buffers (256KB+) can block the main thread
// for 5+ seconds while the WebGL renderer processes ReadPixels synchronously.
// Instant cache restore for IDLE sessions only.
// For busy sessions, the cache is always stale — writing it first causes a
// jarring double-render: stale content appears, then the terminal flashes
// blank and rewrites with fresh data. Skip the cache and write the fresh
// buffer once for a single clean transition.
const cachedBuffer = this.terminalBufferCache.get(sessionId);
if (cachedBuffer) {
const sessionIsBusy = session && (session.status === 'busy' || session.status === 'working');
if (cachedBuffer && !sessionIsBusy) {
_crashDiag.log(`CACHE_WRITE: ${(cachedBuffer.length/1024).toFixed(0)}KB`);
this.terminal.clear();
this.terminal.reset();
@@ -2165,6 +2168,11 @@ class CodemanApp {
if (selectGen !== this._selectGeneration) { if (this._isLoadingBuffer) this._finishBufferLoad(); this._restoringFlushedState = false; return; }
this.terminal.scrollToBottom();
_crashDiag.log('CACHE_DONE');
} else if (sessionIsBusy) {
// Clear stale content immediately — fresh buffer is being fetched
this.terminal.clear();
this.terminal.reset();
_crashDiag.log('CACHE_SKIP_BUSY');
}
_crashDiag.log('FETCH_START');
@@ -2206,9 +2214,9 @@ class CodemanApp {
this.terminal.reset();
}
// Buffer load complete — unblock live SSE writes and flush any queued events.
// chunkedTerminalWrite calls _finishBufferLoad internally, but if we skipped
// the chunked write (small buffer, cache hit, or empty), we must call it here.
// Buffer load complete — unblock live SSE writes (queued events are discarded
// to prevent duplicate content). chunkedTerminalWrite calls _finishBufferLoad
// internally, but if we skipped the write (cache hit or empty), call it here.
if (this._isLoadingBuffer) {
this._finishBufferLoad();
}
+7 -7
View File
@@ -1109,18 +1109,18 @@ Object.assign(CodemanApp.prototype, {
},
/**
* Complete a buffer load: unblock live SSE writes and flush any queued events.
* Complete a buffer load: unblock live SSE writes.
* Called when chunkedTerminalWrite finishes (or is skipped for empty buffers).
*
* Queued SSE events are DISCARDED, not flushed. The loaded buffer from the API
* is the source of truth up to the response timestamp. SSE events queued during
* the fetch+write overlap with the buffer — flushing them writes duplicate data
* (especially Ink cursor-up redraws), corrupting the terminal display.
* After unblocking, new SSE/WS events deliver subsequent output normally.
*/
_finishBufferLoad() {
const queue = this._loadBufferQueue;
this._isLoadingBuffer = false;
this._loadBufferQueue = null;
if (queue && queue.length > 0) {
for (const data of queue) {
this.batchTerminalWrite(data);
}
}
},