Persist terminal sessions across quit with zmx#103
Open
thdxg wants to merge 7 commits into
Open
Conversation
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.
- 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.
Contributor
Deploying with
|
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
zmx attach macterm-<sessionID>, injected via ghostty's newcommand-wrapperconfig. The wrapper argv is prepended to the fully resolved shell command (afterlogin(1)+ shell integration), so OSC 7 cwd / OSC 133 framing — and thus live tab titles and cwd-on-split — stay intact. (Thecommand-wrapperpatch is carried by thethdxg/ghosttyfork as a single rebased commit;thdxg/zmxbuilds the prebuilt binary the same way.)Pane.sessionID(distinct from the restore-regeneratedPane.id) is persisted inPaneSnapshotalong with the live cwd. On relaunch the restored pane reattaches its daemon instead of spawning fresh..macterm/layout.yamlonly seeds a genuine first open (no snapshot), never overwrites a reattached session.macterm-*orphans left by a crash/force-quit, sparing co-resident sessions from other apps.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
ZmxClientTests— parser/budget/launch/reaper;WorkspaceSerializerTests— sessionID/cwd round-trip + back-compat;AppStateTests— reopen-restores-silently + first-open-auto-applies). Lint + format clean.zmx lsshows onemacterm-*session per pane with shell integration intact; reattach across quit; layout remembered; co-residentsupa-*sessions untouched.Note for reviewers
Depends on the
command-wrapperfield now in thethdxg/ghostty--latestGhosttyKit, whichsetup.shalready pulls — no manual steps.🤖 Generated with Claude Code