Skip to content

feat(desktop): persist & restore terminal tabs + scrollback across relaunch - #54585

Merged
OutThisLife merged 1 commit into
mainfrom
bb/desktop-terminal-history
Jun 29, 2026
Merged

feat(desktop): persist & restore terminal tabs + scrollback across relaunch#54585
OutThisLife merged 1 commit into
mainfrom
bb/desktop-terminal-history

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

Summary

The desktop app's user terminal tabs are wiped on every restart. This adds VS Code-style persistence: tabs reopen on launch with their recent scrollback replayed above a fresh shell. Processes are not revived — a new shell starts one line below the restored history (no fake "reconnected" state).

Tabs, the active selection, each tab's cwd, and a serialized scrollback snapshot are persisted to localStorage (hermes.desktop.terminals.v1) synchronously on every change, so a snapshot is always on disk before the window tears down — no fragile unload-time flush.

How it works

  • Capture (use-terminal-session.ts): a SerializeAddon snapshots the buffer on a 750 ms leading-edge throttle — the first output after an idle gap persists almost immediately (so cmd; quit is captured), then at most once per window while output streams.
  • Trim (cleanReviveSnapshot): the snapshot's trailing idle prompt is stripped (the short block after the shell's last blank line) so the restore doesn't show the old prompt directly above the fresh one. Only a short tail is dropped — real command output is never trimmed, and prompts without a leading blank just keep the historical prompt.
  • Budget: capped at 200 serialized scrollback lines and 48k chars (tail-trimmed) so a long line-wrapped buffer can't blow the storage quota.
  • Replay (workspace.tsxinstance.tsx → hook): the snapshot is written once on mount, then a newline, then the fresh shell boots beneath it.
  • Agent mirror tabs stay runtime-onlypersistTerminals filters to kind === 'user', and updateTerminalReviveBuffer ignores agent tabs.

Key correctness fix — no data loss on quit

App quit / reload / window-close kills the PTYs from the main process, which fires onExit in the renderer. But React skips effect cleanups on teardown, so the per-instance disposed flag never flips — the old onExit path would then call closeTerminal() for every tab and wipe the persisted list right before relaunch reads it (symptom: "2nd terminal missing after Cmd+Q", intermittent). A module-level pagehide/beforeunload guard now suppresses onExit-driven closeTerminal during teardown. A genuine exit / Ctrl-D still closes the tab.

Touch points

File Change
terminal/terminals.ts Persist/restore store: load on init, sync write on every change, updateTerminalReviveBuffer, cap
terminal/use-terminal-session.ts SerializeAddon capture + throttle, snapshot trim, replay, teardown guard
terminal/instance.tsx, terminal/workspace.tsx Thread reviveBuffer through to the hook
apps/desktop/package.json, package-lock.json @xterm/addon-serialize
terminal/terminals.test.ts Persistence unit tests

Test plan

  • tsc -p . --noEmit clean
  • Unit tests (terminals.test.ts): restore tabs/active/history on load; sync persist; agent tabs never persist; oversized buffer tail-trimmed; storage cleared when all tabs close
  • Open 2 tabs, run commands, Cmd+Q → reopen: both tabs return with history (no tab loss)
  • Repeat Cmd+Q several times: history/prompt does not stack or duplicate
  • Manual exit / Ctrl-D in a tab still closes just that tab
  • Alt+F4 (Windows/Linux) and window-close behave like Cmd+Q
  • Agent background-process mirror tabs do not reappear after restart

…launch

User terminal tabs and their recent scrollback now survive an app restart
(VS Code parity). Tabs, active selection, cwd, and a serialized scrollback
snapshot are written to localStorage on every change; on launch the tabs
reopen with their history replayed above a fresh shell. Processes are NOT
revived — a new shell starts one line below the restored block.

- Capture: SerializeAddon snapshots the buffer on a 750ms leading-edge
  throttle, so a `cmd; quit` lands on disk before teardown; the snapshot is
  trimmed of its trailing idle prompt (no "double prompt" on restore) and
  capped (200 scrollback lines / 48k chars) to stay under the storage budget.
- Teardown guard: app quit/reload kills the PTYs from the main process,
  firing onExit in the renderer, but React skips effect cleanups on teardown
  so the per-instance `disposed` flag never flips. A pagehide/beforeunload
  flag stops onExit from calling closeTerminal() and wiping the persisted
  tabs right before relaunch restores them. A real `exit`/Ctrl-D still closes.
- Agent mirror tabs stay runtime-only — only user tabs persist.
@OutThisLife
OutThisLife requested a review from a team June 29, 2026 03:13
@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have labels Jun 29, 2026
@OutThisLife
OutThisLife enabled auto-merge June 29, 2026 03:43
@teknium1

Copy link
Copy Markdown
Contributor

Reviewed — this is clean, well-scoped desktop work. Verified against current main:

Premise holds. It follows the exact app persistence convention — hermes.desktop.terminals.v1 via readKey/writeKey, synchronous write on every store change, same pattern as preview.ts and composer-queue.ts. No core files touched; everything stays under apps/desktop/.

Validation:

  • tsc -p . --noEmit → clean
  • terminals.test.ts → 5/5 passing under the desktop runner (vitest run --environment jsdom)
  • @xterm/addon-serialize@^0.14.0 resolves

Nice catch on the teardown bug. The pagehide/beforeunload guard correctly handles the case where app quit kills PTYs from the main process and fires onExit, but React skips effect cleanups on teardown so disposed never flips — without the guard, onExit would call closeTerminal() and wipe the persisted list right before relaunch reads it. A genuine exit/Ctrl-D still closes the tab (flag stays false). Sound.

One nit (non-blocking): the new test file has no // @vitest-environment jsdom pragma, but that matches ~10 sibling store tests that rely on the test:ui script's --environment jsdom flag, so it runs green in CI. Consistent with existing convention, not a defect.

Approach is right (snapshot + replay, no fake process revival). Authorship will be preserved via rebase-merge when we land it. Thanks @OutThisLife!

@OutThisLife
OutThisLife merged commit fb0644f into main Jun 29, 2026
21 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-terminal-history branch June 29, 2026 04:38
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…rminal-history

feat(desktop): persist & restore terminal tabs + scrollback across relaunch
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…rminal-history

feat(desktop): persist & restore terminal tabs + scrollback across relaunch
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…rminal-history

feat(desktop): persist & restore terminal tabs + scrollback across relaunch
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…rminal-history

feat(desktop): persist & restore terminal tabs + scrollback across relaunch
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…rminal-history

feat(desktop): persist & restore terminal tabs + scrollback across relaunch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants