Add mutation verbs to the macterm CLI (create, split, run, grid)#142
Merged
Conversation
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
Contributor
Window-state benchmark
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 |
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.
18 tasks
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#165–thdxg#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>
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.
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
mainwith #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, plusdocs/cli.md. Two run paths kept deliberately distinct (spawn-timeinitial_inputvia--runvs typing into a live shell viapane run/sendText); headless callers get typedbusyerrors instead of UI confirmation dialogs; every pane's env carriesMACTERM_SESSIONfor self-targeting. Fully unit-tested and verified live against a hermetic instance — details in #140.🤖 Generated with Claude Code