You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug: two windows on the same chat — terminal size state desyncs across primary flips (stale lastPushed dedupe, dual-primary push race, wrong-cols replay) #313
With two windows showing the same chat tab, the terminal's size state can desync when primary ownership flips between windows. Three mechanisms, from code inspection of the impl 0018/0020 path:
Wrong-cols replay on every flip (by design, with a real cost). A secondary window mounts nothing (ChatPaneGroup.tsxrenderTerminalPane: secondaryFor(id)?.secondary → null), so a secondary→primary flip is a fresh xterm mount that replays ring bytes recorded at the other window's cols into this window's grid. The live region heals (attach dance → cols push → fix(session): terminal history loss on tab return — ring purge cols-gate + measurable WebGL attach #308 purge → SIGWINCH repaint), but the re-wrapped replay stays as garbled scrollback, accumulating with every flip between differently-sized windows.
Size state is frontend-local and never reconciled. Each RunnerTerminal instance dedupes pushes against its own lastPushedColsRef/lastPushedRowsRef (RunnerTerminal.tsx:206), and session_resize is push-only — there is no API to read the PTY's actual size (api.ts:309, commands/session.rs:109). When the other window resizes the PTY, this window's refs go stale: refresh-push-skip (RunnerTerminal.tsx:362-371) can then skip a push the PTY actually needs. Same stale-size class as bug: resuming an old claude-code terminal shows a black screen with cursor at top until a manual window resize #312.
Dual-primary push race during flips. Primary is derived from the debounced (80ms), broadcast focus map (windowFocus.ts), so there's an interval during a flip where both windows are mounted and both push conflicting sizes. Last write wins — and the loser can be the window that stays primary, leaving the PTY at the other window's size while nothing on the primary re-pushes (its rect didn't change, so no ResizeObserver event). The agent then repaints at the wrong width into the primary's grid until a manual window resize.
Expected behavior
The primary window's grid and the PTY size converge without a manual resize, and flips between differently-sized windows don't accumulate garbled scrollback.
Suggested direction
An authoritative size read-back — either a session_pty_size query or the current size echoed on resize acks / session events — lets the activation path reconcile against the PTY's real size instead of frontend-local lastPushed refs. That single primitive addresses (2), (3), and the refresh-push-skip half of #312.
Relevant code
src/components/ChatPaneGroup.tsx — renderTerminalPane secondary gate (unmount/remount on flip).
Description
With two windows showing the same chat tab, the terminal's size state can desync when primary ownership flips between windows. Three mechanisms, from code inspection of the impl 0018/0020 path:
Wrong-cols replay on every flip (by design, with a real cost). A secondary window mounts nothing (
ChatPaneGroup.tsxrenderTerminalPane:secondaryFor(id)?.secondary → null), so a secondary→primary flip is a fresh xterm mount that replays ring bytes recorded at the other window's cols into this window's grid. The live region heals (attach dance → cols push → fix(session): terminal history loss on tab return — ring purge cols-gate + measurable WebGL attach #308 purge → SIGWINCH repaint), but the re-wrapped replay stays as garbled scrollback, accumulating with every flip between differently-sized windows.Size state is frontend-local and never reconciled. Each
RunnerTerminalinstance dedupes pushes against its ownlastPushedColsRef/lastPushedRowsRef(RunnerTerminal.tsx:206), andsession_resizeis push-only — there is no API to read the PTY's actual size (api.ts:309,commands/session.rs:109). When the other window resizes the PTY, this window's refs go stale:refresh-push-skip(RunnerTerminal.tsx:362-371) can then skip a push the PTY actually needs. Same stale-size class as bug: resuming an old claude-code terminal shows a black screen with cursor at top until a manual window resize #312.Dual-primary push race during flips. Primary is derived from the debounced (80ms), broadcast focus map (
windowFocus.ts), so there's an interval during a flip where both windows are mounted and both push conflicting sizes. Last write wins — and the loser can be the window that stays primary, leaving the PTY at the other window's size while nothing on the primary re-pushes (its rect didn't change, so no ResizeObserver event). The agent then repaints at the wrong width into the primary's grid until a manual window resize.Expected behavior
The primary window's grid and the PTY size converge without a manual resize, and flips between differently-sized windows don't accumulate garbled scrollback.
Suggested direction
An authoritative size read-back — either a
session_pty_sizequery or the current size echoed on resize acks / session events — lets the activation path reconcile against the PTY's real size instead of frontend-locallastPushedrefs. That single primitive addresses (2), (3), and therefresh-push-skiphalf of #312.Relevant code
src/components/ChatPaneGroup.tsx—renderTerminalPanesecondary gate (unmount/remount on flip).src/components/RunnerTerminal.tsx:206,362-371—lastPushed*Refdedupe;:1001-1091replay drain (wrong-cols rewrap).src/lib/windowFocus.ts— debounced focus map →isSecondaryFor(dual-primary interval).src-tauri/src/commands/session.rs:109—session_resize(push-only; no size query).Environment