Repository navigation
fix(terminal): keep the alternate screen across a pane remount (#345) - #348
Merged
Merged
Conversation
…ehmam#345) A split-tree change remounts a pane, and the remount snapshot excluded the alternate buffer. The replacement xterm came up on the normal buffer while the TUI kept running, so its frames landed in the shell's scrollback and its exit ESC[?1049l only homed the cursor, printing the prompt over the stale frame. Serialize with the alternate buffer included: SerializeAddon writes the normal buffer and its cursor, then ESC[?1049h ESC[H and the alternate screen, so the replacement comes back on the alternate buffer with the shell's cursor saved.
amirlehmam
pushed a commit
that referenced
this pull request
Oct 25, 2026
…om (#360) Since #346 a surface's xterm lives as long as the surface, so the remount snapshot is mostly taken when a pane is really closed. That snapshot stayed cached and was replayed into the next terminal with the same surface id, and Load session keeps saved ids. Since #348 the snapshot carries the alternate screen, so closing a pane that ran vim or Claude Code fullscreen and then loading a session that held it put the new shell's prompt on the alternate buffer, with its output kept out of scrollback. The cache is now RemountSnapshotCache (utils/remount-snapshot.ts): - A terminal whose surface has left the store is not serialized, and any earlier entry for it is dropped. A close no longer serializes the whole scrollback during unmount. - The replay waits for the PTY lookup and runs only when the PTY was reused (pty.has, or create's `reused` for StrictMode's second mount). A fresh PTY drops the entry. The per-surface mouse modes had the same flaw: a closed TUI's modes were replayed into a fresh shell on its id, which then tracked the mouse in SGR, and the wheel handler forwarded scrolling to the shell as escape codes. They are now replayed on the same reuse decision (takeMouseModeReplay), and a fresh PTY forgets the surface's modes and wheel remainder. The machinery stays for terminals rebuilt on a live PTY: the root ErrorBoundary's retry remounts every terminal in production, and StrictMode and Fast Refresh do it in dev. Because the hold now starts after the async lookup, the write callback re-fits whatever the pane size is instead of only when a sync was refused. Refs #345
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #345
Problem
When a split-tree change remounts a pane while a fullscreen TUI (Claude Code, opencode, vim) is on the alternate screen, the replacement xterm comes up on the normal buffer. The TUI keeps running and never re-sends
ESC[?1049h, so its frames land in the shell's scrollback, and a full-screen clear wipes the shell's own lines. On exit, itsESC[?1049lhas no alternate screen to leave and only restores the cursor, so the shell prompt is printed over the stale frame.The cause is
snapshotSurfaceBuffer, which serialized withexcludeAltBuffer: true. The comment there expected the TUI's post-remount redraw to cover it, but the redraw happens without the DECSET.Fix
The remount snapshot now includes the alternate buffer. SerializeAddon writes the normal buffer and its cursor first, then
ESC[?1049h ESC[Hand the alternate screen. So the replacement comes back on the alternate buffer, and?1049hsaves the shell's real cursor position for the TUI's exit. The serialize call moved intoutils/remount-snapshot.ts, so the regression test can run it against real xterm and SerializeAddon without a DOM. The comments inuseTerminal.tsandPinnedPrompt.tsxthat described the old gap are updated, since the buffer type now survives a remount.Verifying it by hand
The Vite dev server runs React StrictMode, whose double mount wipes a pane's buffer on every remount even for a plain shell, so test this against a production renderer build (
npm run build:renderer, served withvite preview).cls; "MAIN-1"; "MAIN-2"; [Console]::Write("e[?1049he[H"); 1..5|%{"ALT-$_"}; Start-Sleep 12; [Console]::Write("e[?1049l"); "EXITED"`.ALT-*lines, and after the exit it should showMAIN-1,MAIN-2,EXITEDand the prompt below them.The same check with Claude Code (split and close next to it, then
/exit) returns to the pre-launch screen.Related: #347 is the same end state reached through Reset terminal state, and #346 proposes keeping terminals alive across remounts.