Skip to content

Add mutation verbs to the macterm CLI (create, split, run, grid)#142

Merged
thdxg merged 3 commits into
mainfrom
claude/mystifying-colden-103489-pr2
Jul 5, 2026
Merged

Add mutation verbs to the macterm CLI (create, split, run, grid)#142
thdxg merged 3 commits into
mainfrom
claude/mystifying-colden-103489-pr2

Conversation

@thdxg

@thdxg thdxg commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Replaces #140 (auto-closed when its base branch was deleted by #139's merge; GitHub can't reopen a PR whose base is gone). Same content, now based on main with #139's squash merged in. Original description and review notes: #140.

TL;DR: makes the control plane actually drive the app — project create/select, tab new/select/close, pane split/focus/close/run, grid RxC, session kill, layout apply/save, plus docs/cli.md. Two run paths kept deliberately distinct (spawn-time initial_input via --run vs typing into a live shell via pane run/sendText); headless callers get typed busy errors instead of UI confirmation dialogs; every pane's env carries MACTERM_SESSION for self-targeting. Fully unit-tested and verified live against a hermetic instance — details in #140.

🤖 Generated with Claude Code

thdxg added 3 commits July 5, 2026 00:44
External apps and AI agents need to drive Macterm programmatically
(#107), and the CI benchmark needs to spawn realistic workloads
headlessly. Darwin notifications (BenchmarkControl) carry no payload or
response, so this adds a Unix-socket control plane: newline-delimited
JSON requests dispatched on the main actor into the same AppState paths
the UI uses, and a bundled ArgumentParser CLI at
Contents/Resources/bin/macterm (PATH-injected into every pane along
with MACTERM_SOCKET).

This first slice ships the protocol, server, and read-only verbs —
status, project/tab/pane list, session list/info. Mutations (create,
select, split, run) and the benchmark workload follow in separate PRs.

Design notes drawn from cmux/Zentty/Supacode source: socket-ownership
check before connect, env var as discovery hint (never a hard pin, so a
stale export can't brick shells after an app restart), non-blocking
connect with a short poll, error responses carrying recovery hints, and
a safe-fail CLI contract (stdout only on success; exit 2 when the app
is unreachable).
With the control plane in place, this teaches it to actually drive the
app: project create/select (idempotent by canonical path, ProjectPath-
validated, remote specs rejected until #104), tab new/select/close,
pane split/focus/close/run, an equal-cells grid verb (the benchmark
workhorse), session kill, and layout apply/save.

Semantics worth reviewing:
- New panes/tabs take a command via the layout run: path (spawn-time
  initial_input); pane run types into an EXISTING live shell through a
  new GhosttyTerminalNSView.sendText — the paste path, guarded so a
  missing surface is a typed no_surface error, never a silent drop.
- Headless callers can't answer dialogs, so close/apply verbs return a
  typed busy error where the UI would stage a confirmation; --force
  takes the destructive path deliberately.
- Every pane's env now carries MACTERM_SESSION (its restart-stable zmx
  session name), so macterm run inside a pane self-targets. pane close
  deliberately ignores that fallback: destroying "whatever pane I'm
  in" by omission is a footgun.

docs/cli.md documents the grammar, targeting rules, wire protocol for
non-CLI clients, and the same-user security posture.
…den-103489-pr2

# Conflicts:
#	AGENTS.md
#	CLI/MactermCommand.swift
#	Macterm/Control/ControlHandler.swift
#	Macterm/Control/ControlProtocol.swift
#	MactermTests/Control/ControlHandlerTests.swift
@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:docs Documentation labels Jul 5, 2026
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@461ec4008 this branch Δ
focused CPU % 0.90 0.90 +0%
Memory (RSS MB) 107.7 108.3 +1%
CPU ms/s (powermetrics) 8.9 8.7 -3%
Wakeups/s (powermetrics) 177.2 169.2 -5%
unfocused CPU % 1.23 0.93 -24%
Memory (RSS MB) 107.1 112.7 +5%
CPU ms/s (powermetrics) 12.1 8.9 -26%
Wakeups/s (powermetrics) 149.3 153.8 +3%
minimized CPU % 0.17 0.13 -20%
Memory (RSS MB) 107.4 112.9 +5%
CPU ms/s (powermetrics) 1.8 1.6 -14%
Wakeups/s (powermetrics) 64.4 66.8 +4%

30s sampling window per state; CPU % is the process CPU-time delta over the window. Runs land on different shared runners, so treat small deltas as noise — 🔺/🔻 marks changes ≥25% that also clear the metric's absolute noise floor (CPU % ≥0.5, Memory (RSS MB) ≥25, CPU ms/s ≥5, Wakeups/s ≥50), and those add the benchmark:regression / benchmark:improvement label.

@thdxg
thdxg merged commit 3e6caa7 into main Jul 5, 2026
7 checks passed
@thdxg
thdxg deleted the claude/mystifying-colden-103489-pr2 branch July 5, 2026 01:37
thdxg added a commit that referenced this pull request Jul 6, 2026
Post-merge with main's website redesign (#143): a comprehensive Remote
Projects page (requirements, ssh-config aliasing with a ControlMaster
recipe, the sheet + palette flows, behavior table, remote layouts with
zmxPath, troubleshooting keyed to the pane diagnostics, limitations),
and a macterm CLI page adapted from docs/cli.md — the CLI (#139/#142)
had a repo doc but no website page. Existing pages get the remote
cross-references: persistence (remote sessions survive local reboots),
layouts (remote path: + zmxPath), palette (remote specs in path mode),
intro. Also fixes main's new ControlHandlerTests ZmxClient doubles for
the remote closure arity from this branch.
armyers pushed a commit to armyers/CYOTE-arm that referenced this pull request Jul 15, 2026
)

* Paste clipboard images into TUIs instead of dropping them (thdxg#109)

* Fix activity detection for in-place terminal output (thdxg#108)

* fix: consistent slider width for each settings screen (thdxg#116)

* fix: correct off-by-one in search match counter (thdxg#122)

* fix: render CJK IME preedit text (thdxg#119)

* Turn off continuous rendering for panes that are not visible in the app. (thdxg#115)

Co-authored-by: Aryeh <aryeh@studio.local>

* Skip quiet-settle for occluded panes so parked renderers can't fake completion (thdxg#123)

* Bundle zmx and add the session client library (no behavior change) (thdxg#124)

* Poll foreground processes adaptively instead of at a fixed 250ms (thdxg#126)

* Tick libghostty on wakeup instead of a 120Hz timer (thdxg#125)

* Isolate UserDefaults under test so runs can't corrupt the developer's app state (thdxg#128)

* Wrap terminal surfaces in zmx sessions with daemon-side foreground resolution (thdxg#127)

* Silence Swift 6 non-Sendable warnings on AppState's poll-event observers (thdxg#130)

* Benchmark window-state resource usage in CI and compare PRs against main (thdxg#129)

* Persist zmx sessions across quit and reattach on relaunch (thdxg#131)

* Spell out the noise floors in the benchmark footnote (thdxg#132)

* Surface zmx unavailability in Settings and document session persistence (thdxg#133)

* Break the title-replay render loop for good (thdxg#134)

* Make Unload Project stop the project's shells again (keeping layout) (thdxg#135)

* Fix arrow-key shortcuts never matching live keypresses (thdxg#136)

* Centralize project layout files in ~/.config/macterm/projects (thdxg#114) (thdxg#137)

* Add a control socket and bundled macterm CLI (read-only verbs) (thdxg#139)

* Add mutation verbs to the macterm CLI (create, split, run, grid) (thdxg#142)

* Sample the benchmark under a real workload via the macterm CLI (thdxg#141)

* Redesign website landing + docs pages (thdxg#143)

* Add CLI reference to the docs site (thdxg#145)

* Remote projects over SSH backed by zmx (thdxg#104) (thdxg#138)

* Cache build artifacts across CI to cut cold-build time (thdxg#146)

* Fix release export failing on Xcode 26 (mac-application method rejected) (thdxg#147)

* Refresh user docs, README, and landing page for remote projects, CLI, and persistence (thdxg#149)

* Keep main-window hotkeys from firing while another window is key (thdxg#148)

* fix: open existing project when same path is selected (thdxg#150)

* Track the system appearance instead of reading back our own pinned one (thdxg#152)

* chore(deps): bump the actions group with 3 updates (thdxg#151)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Pin the website footer to the viewport bottom on short pages (thdxg#153)

* Gate appcast and Homebrew tap on the release prerelease flag (thdxg#155)

* Add a settings slider for unfocused split pane dimming (thdxg#156)

* Report benchmark results from a workflow_run so fork PRs can be labeled (thdxg#157)

* Stop wide docs code blocks and tables from overflowing the page (thdxg#162)

* Audit remediation + sidebar tab/project drag-and-drop, OSC 2 title fixes (thdxg#158)

* Add a Copy Session ID action for handing a pane's zmx name to an LLM (thdxg#163)

* Make the window-state benchmark tolerant of shared-runner CPU noise (thdxg#164)

* Terminal-state introspection + control-socket verbs (thdxg#165thdxg#167) and a fork-drift guard (thdxg#168) (thdxg#169)

* Re-graft reboot-resurrect onto the sessionName-keyed model (stage 2)

Stage 2 of the upstream v1.20.4 merge: restore the fork's post-reboot
resurrect (capture color scrollback + restartable command, replay into
fresh sessions after a reboot), re-keyed from the old bare-UUID sessionID
to upstream's persisted `sessionName`, and driven through upstream's
off-main zmx seam.

Correctness fix (the reason it couldn't just be re-wired): resurrect's
history/print/send must hit the SAME daemon the panes attach through.
ZmxService now resolves the bundled zmx (Contents/Resources/zmx/zmx)
first and pins ZMX_DIR = ZmxSocketBudget.socketDir() in run(), matching
ZmxClient/ZmxAttach — the old fork ZmxService used homebrew paths and no
ZMX_DIR, so it would have talked to the wrong (or no) daemon.

- ZmxService: bundled binary + ZMX_DIR pin; history/print/send params
  renamed sessionID -> sessionName.
- ResurrectStore + SessionResurrect: keyed by sessionName (macterm-slug-hex
  is a safe filename); seed guard drops the vestigial sessionPersistence
  toggle (upstream always persists) — gates on didReboot + zmx only.
- PaneSnapshot.resurrectCommand + a transient Pane.resurrectCommand, kept
  distinct from Pane.command (layout run:) so a plain-quit reattach never
  re-runs it — only a genuine reboot does.
- AppState: ~15s capture timer started once at launch (independent of the
  poll cadence), capturing off-main via zmx.sessionListSnapshot leaders +
  ProcessInspector.foregroundCommand + ZmxService.history; skipped under
  XCTest. saveWorkspaces threads the cache into the snapshot.
- SplitNode.ensureNSView seeds local non-ephemeral panes post-reboot.
- Settings: re-added the "Resurrect scrollback after a reboot" toggle +
  per-pane restore-MB stepper (upstream @State style).

Verified end-to-end via the forceReboot sim (pkill zmx + relaunch):
capture wrote 44KB-504KB color .vt files keyed by sessionName, and the
seed replayed them into fresh sessions (e.g. 504KB in 172 paced chunks)
with no timeouts. 58 test suites pass; format + lint clean.

* Name zmx sessions after the context, show context in session list (stage 3)

Stage 3 of the upstream v1.20.4 merge: make session naming context-centric
(the fork's unit of work is the Context = Project chosen in the cmd-shift-p
picker, not a git repo), and surface the live context in the control CLI.

Part A - context-name slug. A pane's zmx session name derived its slug from
projectPath.lastPathComponent, so a context named "Billing API" opened at
/code/billing-svc became macterm-billingsvc-<hex>, ignoring the chosen name.
Now the project's context name is threaded as the slug source (Pane.init
already took sessionSlug; the gap was callers passing nil):
- Workspace.init / createTab gain sessionSlug, forwarded to the pane.
- AppState.ensureWorkspace(contextName:) + createTab(sessionSlug:) pass
  project.name at every interactive open/new-tab path.
- ControlHandler.tabNew passes project.name too.
- Splits already inherit the source pane's slug; restored panes keep their
  persisted name verbatim (rename-proof, since context is stable at creation).
The path-basename remains the fallback when no context name is supplied.

Part B - context column in `session list`. ControlSessionInfo gains a
`context` field, populated by joining sessionName -> live pane -> project
name (authoritative and rename-proof, not parsed from the slug). Rendered
as a `ctx:<name>` column (`ctx:-` for an orphan session with no live pane);
also present in --json. Docs updated.

Verified end-to-end: a new tab in context "ecs-managed-instances" (path
basename "code") spawned macterm-ecsmanagedin-<hex>, and `session list`
showed `ctx:ecs-managed-instances` on every attached session including the
old basename-slugged restored ones. 59 test suites pass; format + lint clean.

* Verify bundled zmx has no single-print truncation; document (stage 4)

Stage 4 of the upstream v1.20.4 merge: validate whether the bundled zmx
still needs resurrect's defensive print-chunking.

Empirically probed the bundled thdxg/zmx 0.6.0 with isolated single prints
(throwaway ZMX_DIR, detached `zmx run` session): rendered content scales
linearly past 4KB — 3KB→50/50 lines, 9KB→145/150, 18KB→287/300 — with no
hard ~4096-byte cliff (the <5% loss grows with size, i.e. scrollback-window
trimming, not IPC truncation). So the old stock-zmx bug (a single `print` in
the ~4-8KB band silently dropped after one 4096-byte read) is NOT present in
the binary Macterm ships.

Decision: keep the sub-4KB `chunkOnLines` + pacing anyway. It's belt-and-
suspenders — the `zmxPath` override can point resurrect at an older/stock
daemon that still truncates, and the reboot sim already showed chunked replay
handles 500KB cleanly. No behavior change; comments in ZmxService.print and
SessionResurrect updated to reflect the measured reality (bundled = no cliff,
chunking retained defensively for override/older binaries).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Dejan Žegarac <dejanzegarac4@gmail.com>
Co-authored-by: Mateusz Urban <mateuszurban2@gmail.com>
Co-authored-by: Jungwoong Kim <48355099+jungwngkim@users.noreply.github.com>
Co-authored-by: Juno Choi <junochoi2003@gmail.com>
Co-authored-by: takumaru <49429291+takuma-ru@users.noreply.github.com>
Co-authored-by: aryeh <aryeh@users.noreply.github.com>
Co-authored-by: Aryeh <aryeh@studio.local>
Co-authored-by: Ethan (Taehoon) Lee <48551278+thdxg@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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