Skip to content

fix: preserve terminal buffer content across float/dock moves and grid rebuilds#76

Merged
InbarR merged 2 commits into
InbarR:mainfrom
yodobrin:fix/preserve-terminal-buffer-on-move
Apr 26, 2026
Merged

fix: preserve terminal buffer content across float/dock moves and grid rebuilds#76
InbarR merged 2 commits into
InbarR:mainfrom
yodobrin:fix/preserve-terminal-buffer-on-move

Conversation

@yodobrin
Copy link
Copy Markdown
Contributor

Problem

When a terminal is moved between tiled ↔ floating mode (dock/undock), or when a new terminal is added in grid mode, the terminal content disappears — the pane goes blank. The PTY process is still alive, but the user loses all visible output and scrollback.

Root Cause

TerminalPanel creates an xterm.js Terminal in a useEffect. When moved:

  1. Float ↔ Dock: Terminal removed from tiling tree → added to floatingPanels (or vice versa) → React unmounts old TerminalPanel, mounts new one.
  2. Grid rebuild: buildGridTree() creates new split node IDs → React keys change → existing components remount.

On unmount, term.dispose() destroys the entire xterm buffer.

Fix

Use @xterm/addon-serialize to snapshot the buffer before dispose() and restore it on remount.

  • On cleanup: flush pending PTY data, serialize buffer to cache
  • On init: restore cached buffer at original dimensions, then fit to new container

Cache safety

Protection Detail
Auto-expiry Entries deleted after 10s (configurable). Remounts happen in ~1ms.
Size cap Buffers > 2MB skipped to prevent memory bloat.
Pop semantics Consumed on first read, preventing stale reuse.

Files Changed

  • package.json — added @xterm/addon-serialize
  • src/renderer/terminal-buffer-cache.ts — new module for buffer snapshots
  • src/renderer/components/TerminalPanel.tsx — serialize on cleanup, restore on init

Yoav Dobrin and others added 2 commits April 26, 2026 13:32
…d rebuilds

When a terminal is moved between tiled and floating mode, or when the grid
layout rebuilds (e.g. adding a new terminal), the React TerminalPanel
component unmounts and remounts. The unmount calls term.dispose() which
destroys the xterm.js buffer, leaving the terminal blank.

Fix: use @xterm/addon-serialize to snapshot the buffer before dispose and
restore it when the component remounts. A lightweight cache module manages
the snapshots with a configurable TTL (default 10s) and a 2MB size cap to
prevent memory bloat from orphaned or oversized entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… test

The original PR scheduled a setTimeout per save without tracking it. A
re-save of the same id within EXPIRY_MS would leave the older timer
running, and that older timer would silently delete the fresher
snapshot before the next remount could read it. Realistic trigger: a
rapid float → dock → float in <10s.

Track timers in a parallel Map keyed by terminal id and clear the
previous timer on every save / pop.

Adds an e2e regression spec that drops a sentinel into the focused
pane's xterm buffer, moves it tiled → floating → tiled, and asserts
the sentinel survives both transitions. Locks in the buffer-preservation
behavior so future changes can't silently regress it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@InbarR
Copy link
Copy Markdown
Owner

InbarR commented Apr 26, 2026

Pushed two follow-ups onto this branch (commit 7f41459):

  1. Stale-timer fix in terminal-buffer-cache.ts: the original schedules a setTimeout per save without tracking it. If the same id is saved twice within EXPIRY_MS (e.g. a rapid float → dock → float in <10 s), the first timer would still fire and delete the fresher snapshot, blanking the next remount. Now timers are tracked per id and cleared on every save / pop.

  2. e2e regression spec (tests/e2e/float-buffer-preserved.spec.ts): writes a sentinel into the focused pane's xterm buffer, moves it tiled → floating → tiled, and asserts the sentinel survives both transitions. Locks in the fix so we can't silently regress it later.

Otherwise the original implementation reads cleanly - good catch on flushing pendingData before serialize(). Thanks for the fix!

@InbarR InbarR merged commit 211b2a4 into InbarR:main Apr 26, 2026
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.

2 participants