Skip to content

Persist terminal sessions across quit with zmx#103

Open
thdxg wants to merge 7 commits into
mainfrom
session-persistence
Open

Persist terminal sessions across quit with zmx#103
thdxg wants to merge 7 commits into
mainfrom
session-persistence

Conversation

@thdxg

@thdxg thdxg commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

Terminal sessions now survive app quit and reattach on relaunch, backed by zmx — a Zig session multiplexer that passes raw PTY bytes through (no tmux-style screen re-parse, so latency is one extra unix-socket hop). Closing the app used to kill every shell; now they keep running and come back with their buffer and process intact.

How

  • Wrapping. Each surface's shell runs under zmx attach macterm-<sessionID>, injected via ghostty's new command-wrapper config. The wrapper argv is prepended to the fully resolved shell command (after login(1) + shell integration), so OSC 7 cwd / OSC 133 framing — and thus live tab titles and cwd-on-split — stay intact. (The command-wrapper patch is carried by the thdxg/ghostty fork as a single rebased commit; thdxg/zmx builds the prebuilt binary the same way.)
  • Stable id. Pane.sessionID (distinct from the restore-regenerated Pane.id) is persisted in PaneSnapshot along with the live cwd. On relaunch the restored pane reattaches its daemon instead of spawning fresh.
  • Reopen is silent. The restored session always wins; a committed .macterm/layout.yaml only seeds a genuine first open (no snapshot), never overwrites a reattached session.
  • Lifecycle. Permanent closes (pane/tab/project close, layout-dropped panes) kill the session; transient teardown only detaches. A launch-time reaper sweeps macterm-* orphans left by a crash/force-quit, sparing co-resident sessions from other apps.
  • Quit. Detaches by default; a Settings toggle (Session Persistence → off by default) opts into killing sessions on quit, which also restores the "processes will be killed" prompt.
  • Bundling. The zmx binary is a presence-checked download in setup.sh (like GhosttyKit), embedded via a post-compile phase. Never compiled locally (zig 0.15.2 can't link the macOS 26+/Xcode 26.4 SDK).

Testing

  • 403 unit tests pass (new: ZmxClientTests — parser/budget/launch/reaper; WorkspaceSerializerTests — sessionID/cwd round-trip + back-compat; AppStateTests — reopen-restores-silently + first-open-auto-applies). Lint + format clean.
  • Manually verified live: zmx ls shows one macterm-* session per pane with shell integration intact; reattach across quit; layout remembered; co-resident supa-* sessions untouched.

Note for reviewers

Depends on the command-wrapper field now in the thdxg/ghostty --latest GhosttyKit, which setup.sh already pulls — no manual steps.

🤖 Generated with Claude Code

thdxg added 4 commits June 22, 2026 13:41
Download the prebuilt arm64 zmx binary from the thdxg/zmx release in
setup.sh (alongside GhosttyKit) and embed it into the app bundle via a
post-compile build phase. zmx is never compiled locally — zig 0.15.2
cannot link against the macOS 26+/Xcode 26.4 SDK — so it is a presence-
checked download, mirroring GhosttyKit.
A cache-free wrapper around the bundled zmx binary: session-id scheme,
socket-path budget probe, the zmx ls parser, launch resolution (argv
command-wrapper for interactive surfaces), and pure orphan-selection
logic. Every subprocess call is bounded by a 5s timeout + zombie-reap so
a stuck daemon can never hang the close/quit path.
Wrap each surface's shell in zmx via ghostty's command-wrapper, keyed by
a stable per-pane sessionID persisted in the workspace snapshot (with the
live cwd). On relaunch the restored pane reattaches its still-running
daemon instead of spawning fresh; reopen is silent and the restored
session always wins (a committed layout only seeds a genuine first open).

Permanent closes (pane/tab/project close, layout-dropped panes) kill the
session; transient teardown only detaches. A launch-time reaper sweeps
orphaned macterm-* sessions left by a crash or force-quit, sparing
co-resident sessions from other apps. Quitting detaches by default;
a Settings toggle (off by default) opts into killing sessions on quit.
Add the Session Persistence architecture section and drop the stale
"No process persistence" known-limitation.
@github-actions github-actions Bot added area:ui Views, Settings UI area:terminal Terminal surface, ghostty integration area:state AppState, models, persistence area:tests Test changes area:ci CI workflows, dev tooling area:docs Documentation labels Jun 22, 2026
thdxg added 2 commits June 22, 2026 18:37
- Pull the universal (arm64+x86_64) zmx so the wrapper execs on Intel too,
  not just Apple Silicon (the app ships universal).
- Make terminate-on-quit actually kill: applicationWillTerminate now tears
  down sessions synchronously (ZmxClient.killSessionsBlocking) — detached
  Tasks never ran before the process exited, so the setting was a no-op.
- Drop the dead ZmxAttach declared-command branch (resolveLaunch was always
  called with command:nil) → a focused ZmxAttach.wrapperArgv.
- Add an injectable ZmxClient on AppState so the launch reaper is testable;
  cover the reapOrphans driver.
- Tidy: correct .gitignore + AGENTS.md notes, public log interpolation,
  exclude Resources/zmx from the source glob.
It is the headline feature now — shells survive quit and reattach, not
just layout restore. Sharpen the feature blurbs, meta description, and
hero lede accordingly.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
macterm f55cf4c Commit Preview URL

Branch Preview URL
Jun 22 2026, 09:37 AM

A wrapped pane's shell runs under the zmx daemon, a process tree
detached from the zmx-attach client libghostty reports as the foreground
pid — so ProcessInspector saw "zmx" for every pane, naming all tabs zmx
and capturing junk run: values into saved layouts (which then made the
reconciler destroy compliant panes).

Resolve the real foreground per session: a once-per-poll zmx ls maps
sessionID → daemon session-leader pid (cached; daemon pids are stable),
and ProcessInspector reads that leader pty's foreground process group
(tcgetpgrp) — the editor the user sees, not a deeper language-server
leaf. Falls back to libghostty's pid when zmx is bypassed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci CI workflows, dev tooling area:docs Documentation area:state AppState, models, persistence area:terminal Terminal surface, ghostty integration area:tests Test changes area:ui Views, Settings UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant