diff --git a/src/web/public/app.js b/src/web/public/app.js index 01537caa..e1e6fc78 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1362,6 +1362,8 @@ class CodemanApp { this.writeFrameScheduled = false; this._isLoadingBuffer = false; this._loadBufferQueue = null; + // Abort any in-flight chunkedTerminalWrite (SSE reconnect reloads buffers) + this._chunkedWriteGen = (this._chunkedWriteGen || 0) + 1; // Preserve local echo overlay text across SSE reconnect — just hide until // terminal buffer reloads and prompt is visible again. _render() re-scans // for the ❯ prompt on every call, so rerender() after buffer load repositions it. @@ -2020,6 +2022,10 @@ class CodemanApp { this.writeFrameScheduled = false; this._isLoadingBuffer = false; this._loadBufferQueue = null; + // Abort any in-flight chunkedTerminalWrite from the previous session. + // Without this, old rAF-scheduled chunks continue writing stale data + // into the terminal, interleaving with the new session's buffer. + this._chunkedWriteGen = (this._chunkedWriteGen || 0) + 1; // End any in-flight IME composition. // iOS Safari keeps autocorrect composing; switching tabs without ending it // leaves xterm's _compositionHelper._isComposing stuck true, which blocks @@ -2136,6 +2142,11 @@ class CodemanApp { this._isLoadingBuffer = true; this._loadBufferQueue = []; try { + // Fit terminal to container BEFORE writing any buffer data. + // If the browser was resized while viewing another session, the terminal + // 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. diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 0b206f24..df00b1b4 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -219,6 +219,9 @@ Object.assign(CodemanApp.prototype, { // Welcome message this.showWelcome(); + // Generation counter for chunkedTerminalWrite — aborts stale writes on tab switch + this._chunkedWriteGen = 0; + // Handle resize with throttling for performance this._resizeTimeout = null; this._lastResizeDims = null; @@ -1033,6 +1036,10 @@ Object.assign(CodemanApp.prototype, { * @returns {Promise} - Resolves when all chunks written */ chunkedTerminalWrite(buffer, chunkSize = TERMINAL_CHUNK_SIZE) { + // Generation counter: if a newer chunkedTerminalWrite starts (tab switch), + // older writes abort instead of continuing to push stale data into the terminal. + const writeGen = ++this._chunkedWriteGen; + return new Promise((resolve) => { if (!buffer || buffer.length === 0) { this._finishBufferLoad(); @@ -1049,7 +1056,10 @@ Object.assign(CodemanApp.prototype, { const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, ''); const finish = () => { - this._finishBufferLoad(); + // Only finish if we're still the active write — a newer write owns buffer load state + if (this._chunkedWriteGen === writeGen) { + this._finishBufferLoad(); + } resolve(); }; @@ -1067,6 +1077,12 @@ Object.assign(CodemanApp.prototype, { const _chunkStart = performance.now(); let _chunkCount = 0; const writeChunk = () => { + // Abort if a newer chunked write started (user switched tabs) + if (this._chunkedWriteGen !== writeGen) { + resolve(); + return; + } + if (offset >= cleanBuffer.length) { const _totalMs = performance.now() - _chunkStart; console.log(`[CRASH-DIAG] chunkedTerminalWrite complete: ${cleanBuffer.length} bytes in ${_chunkCount} chunks, ${_totalMs.toFixed(0)}ms total`); @@ -1240,6 +1256,9 @@ Object.assign(CodemanApp.prototype, { * @returns {Promise} */ async sendResize(sessionId) { + // Fit terminal to container before reading dimensions — ensures local + // terminal size matches what we report to the server PTY. + if (this.fitAddon) this.fitAddon.fit(); const dims = this.getTerminalDimensions(); if (!dims) return; // Fast path: WebSocket resize