Add zmx-backed workspace/session persistence

MD
15d ago
+680-50

Summary

This PR implements session persistence in cmux by wiring workspace restore/save to zmx-backed Ghostty surfaces.

At a high level:

  • terminal processes are owned by zmx sessions (not tied to cmux process lifetime)
  • cmux persists workspace layout/state metadata and zmx session mappings
  • on relaunch, cmux restores layout and reconnects to existing zmx sessions when available

Why This Approach

Persistence has two independent concerns:

  1. Process lifetime persistence (shell/session survives app exit)
  2. UI/layout persistence (tabs, panes, focused pane, metadata restore)

This implementation intentionally splits them:

  • zmx handles durable terminal session lifetime and reattach
  • cmux handles workspace structure + per-panel metadata snapshots

That separation keeps each layer focused and avoids embedding process supervision logic into cmux UI state management.

What Changed

1) zmx-backed terminal wiring in cmux

  • TerminalSurface now passes zmx_session/zmx_create into Ghostty surface config.
  • TerminalPanel constructors accept zmx session parameters.
  • New app settings for session persistence:
    • Enable zmx Session Persistence
    • Kill Sessions on Workspace Close

2) Workspace snapshot + restore model

  • Added codable snapshot model in WorkspaceStatePersistence.swift.
  • Persisted data includes:
    • workspace list + selected workspace
    • split tree + divider positions
    • focused pane index
    • panel metadata (type, directory, custom title, pinned, browser URL)
    • per-terminal zmx session names
  • Snapshot is written to:
    • ~/Library/Application Support/cmux/workspace-state.json

3) Deterministic zmx session naming + lifecycle

  • Added stable workspace ID + deterministic naming:
    • cmux-<stable-id-prefix>-<panel-index>
  • Session mapping is assigned across all relevant creation paths:
    • initial workspace terminal
    • new tab terminals
    • split terminals (including auto-created split path)
    • replacement terminal after closing last panel
  • Rehydrates panel index counter on restore to prevent collisions.

4) Restore behavior and resilience

  • Restore reconstructs split tree and panel metadata.
  • For restored terminal panels:
    • if session exists/alive => attach (zmx_create = false)
    • if missing => recreate (zmx_create = true)
  • If zmx is unavailable at launch:
    • layout still restores
    • terminals start fresh (non-zmx reattach path)

5) Mapping consistency fixes

  • zmx mappings are removed on panel close / pane close.
  • Detached surface transfer now carries zmx session mapping and restores it on attach.
  • Restore flow force-closes preexisting tabs before reconstruction to avoid stale residual tabs/metadata.
  • Panel directories are rehydrated from snapshot during restore.

6) Submodule/fork wiring

  • .gitmodules points Ghostty submodule to mdsakalu/ghostty fork branch containing zmx backend support.
  • ghostty.h synced with new zmx fields used by cmux.

Tests / Validation

  • Added regression tests in cmuxTests/CmuxWebViewKeyEquivalentTests.swift covering:
    • initial/replacement panel zmx mapping lifecycle
    • detach/reattach zmx mapping preservation
    • restore path directory rehydration
  • Local validation:
    • Debug build succeeds via xcodebuild
    • target test set for WorkspaceZmxPersistenceRegressionTests passes

Dependency

Related Issues

  • Addresses: #480
  • Follow-up to closed tracking issue: #143

Summary by CodeRabbit

  • New Features

    • Terminal session persistence via zmx with deterministic per-panel names and automatic restoration
    • Settings UI to enable persistence and to kill sessions on workspace close (localized strings added)
    • zmx availability checks and session management (probe/kill) exposed
    • New action to copy a tab/window title to the clipboard
  • Improvements

    • More robust initial presentation and retry behavior for terminals
    • Prevents zmx environment leakage on startup; closes/kills zmx on workspace/window close
  • Chores

    • Updated submodule configuration