8000 fix(terminal): keep the alternate screen across a pane remount (#345) by jpstratman · Pull Request #348 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content

fix(terminal): keep the alternate screen across a pane remount (#345) - #348

Merged
amirlehmam merged 1 commit into
amirlehmam:masterfrom
jpstratman:fix/alt-screen-remount
Oct 24, 2026
Merged

amirlehmam merged 1 commit into
amirlehmam:masterfrom
jpstratman:fix/alt-screen-remount

Conversation

@jpstratman
Copy link
Copy Markdown
Contributor

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, its ESC[?1049l has 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 with excludeAltBuffer: 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[H and the alternate screen. So the replacement comes back on the alternate buffer, and ?1049h saves the shell's real cursor position for the TUI's exit. The serialize call moved into utils/remount-snapshot.ts, so the regression test can run it against real xterm and SerializeAddon without a DOM. The comments in useTerminal.ts and PinnedPrompt.tsx that 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 with vite preview).

  1. In a pane, run cls; "MAIN-1"; "MAIN-2"; [Console]::Write("e[?1049he[H"); 1..5|%{"ALT-$_"}; Start-Sleep 12; [Console]::Write("e[?1049l"); "EXITED"`.
  2. While it sleeps, split a pane next to it and close the split.
  3. The pane should still show the ALT-* lines, and after the exit it should show MAIN-1, MAIN-2, EXITED and 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.

…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
amirlehmam merged commit bc28fac into amirlehmam:master Oct 24, 2026
@jpstratman
jpstratman deleted the fix/alt-screen-remount branch October 24, 2026 04:21
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pane loses alternate-screen state, so exiting a fullscreen TUI draws the shell prompt over the old frame

2 participants

0