Fix session scrollback loss on Sparkle updates and red-X window close
Summary
Two independent bugs caused the session snapshot (including scrollback) to be lost in ways that matched real-world reports of "cmux forgets my terminal content after restart":
- Sparkle update relaunch race.
updaterWillRelaunchApplicationwrapped its cleanup inTask { @MainActor in … }, which schedules the body asynchronously and returns to Sparkle immediately. Sparkle then proceeded to terminate + relaunch while the snapshot save raced withapplicationWillTerminate's own save +TerminalController.stop()sequence. Depending on the interleaving, the update could either skip the scrollback save entirely or overwrite a good snapshot with one captured after terminals had been torn down. Fix: run the body synchronously withMainActor.assumeIsolatedso the snapshot is flushed to disk before Sparkle touches anything else. Pattern matches existing usage inGhosttyTerminalView.swift. - Red-X window close dropped scrollback and deleted the snapshot.
unregisterMainWindowsaved withincludeScrollback: falseandremoveWhenEmpty: truefor every red-X close while the app stayed running. That meant (a) remaining windows' scrollback was cleared on every close, and (b) closing the last window deleted the snapshot file entirely — so the next launch started blank instead of restoring the prior session. Fix: capture scrollback for the remaining windows and keep the snapshot file around even when no windows remain. TheshouldPersistSnapshotOnWindowUnregistergate still skips this save during app termination, whereapplicationWillTerminatehas already written a full snapshot with scrollback. The now-unusedshouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregisterpolicy function and its test assertions are removed.
Scope is intentionally limited to these two fixes. Broader capture-path improvements (scrollback on autosave, sleep/wake observers, etc.) are out of scope for this PR.
Test plan
- Unit: `cmux-unit` scheme — existing
testWindowUnregisterSnapshotPersistencePolicystill passes with the trimmed assertions. - Manual: open multiple windows with terminal content, close one with the red X, verify the remaining window's scrollback survives a Cmd+Q / relaunch cycle.
- Manual: open cmux with terminal content, close the last window with the red X, relaunch the app, verify workspaces + scrollback are restored (previously: empty start).
- Manual: trigger a Sparkle update install-and-relaunch, verify scrollback survives the update.
- Manual: confirm
~/Library/Application Support/cmux/session-*.jsoncontainsscrollbackfields after each of the above flows.
Notes for reviewers
I could not run a full local build — the machine doesn't have zig installed and GhosttyKit.xcframework is not prebuilt. The Swift diff is small (~20 lines across 3 files) and has been reviewed via the code-reviewer agent. Please run CI and a local build before merging.
Summary by cubic
Fixes two cases where session scrollback was lost: during Sparkle update relaunch and when closing windows with the red X. Scrollback now survives updates and relaunches, even after closing the last window.
-
Bug Fixes
- Made
updaterWillRelaunchApplicationrun synchronously viaMainActor.assumeIsolatedso the snapshot is flushed beforeSparkleterminates, avoiding races withapplicationWillTerminate/TerminalController.stop(). - On
unregisterMainWindow, save withincludeScrollback: trueandremoveWhenEmpty: falsewhile the app stays running, preserving scrollback and keeping the snapshot after last-window close; still skipped during app termination.
- Made
-
Refactors
- Removed
shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregisterand its test assertions.
- Removed
Summary by CodeRabbit
Release Notes
-
Bug Fixes
- Improved session persistence reliability when closing windows to ensure data is consistently saved.
- Enhanced the application update relaunch process to guarantee sessions and restorable state are properly persisted before restart, preventing potential data loss during updates.
-
Refactor
- Streamlined internal session management code.