Add a control socket and bundled macterm CLI (read-only verbs)#139
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).
Contributor
Window-state benchmark
|
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.
First of three PRs for #107 (Macterm CLI) — also groundwork for realistic CI benchmark workloads (#129 follow-up). This slice ships the IPC control plane and the read-only command set; mutations (project create/select, tab new, pane split/run, grid, session kill, layout apply/save) come in PR 2, and the benchmark workload integration in PR 3.
What's here
Macterm/Control/ControlProtocol.swift— wire types, compiled into both the app and CLI targets so the codec can't drift. One request per connection: newline-terminated JSON both ways, client half-closes after sending. Requests are{v, id, command, args}withnoun.verbcommands; responses echoidwith{ok, data}or a typed error{code, message, action?}whereactionis a human recovery hint.ControlSocketServer— Unix socket at<App Support>/control.sock(per build flavor; followsMACTERM_BENCHMARK_DATA_DIR). Dedicated thread polling a non-blocking listen fd — never a blockingaccept()on a queue shared withstop(). Per-connection dispatch hops to@MainActor. Starts at launch; requests before AppState exists get astartingerror (so a readiness poll sees a well-formed response).FD_CLOEXECkeeps spawned shells from holding the socket past quit; socket file is 0600 and unlinked on quit.ControlHandler— translate-and-delegate only: resolves selectors (name / UUID / 1-based index for projects and tabs; duplicates surface as typedambiguouserrors) and calls the sameAppState/ProjectStore/ZmxClientpaths the UI uses. Reads zmx viaappState.zmxso tests stub one seam.CLI/(MactermCLItool target →Contents/Resources/bin/macterm) — swift-argument-parser command tree:status,project list,tab list,pane list,session list|info, all with--json. The app prepends the bundled bin dir toPATHand exportsMACTERM_SOCKETbefore any shell spawns, somactermworks inside every pane.Design notes (from reading cmux, Zentty, Supacode source)
MACTERM_SOCKETexport falls through to the well-known release/debug paths instead of bricking every already-spawned shell after an app restart. Only--socketpins.authfield.Gotcha worth knowing: the CLI target's
PRODUCT_NAMEismacterm, butPRODUCT_MODULE_NAMEmust stayMactermCLI— amactermmodule collides case-insensitively with the app'sMacterm.swiftmodulein the shared products dir and breaks the hosted test target's@testable import.Testing
mise run format/lint/testgreen — new suites:ControlProtocolTests(codec round-trips, forward-compat, hand-built CLI JSON),ControlHandlerTests(queries + selector resolution + zmx stubs against realAppStatewith tempdir stores),ControlSocketServerTests(real socket round-trips,startingbefore attach, stale-file replacement, overlong-path refusal, unlink on stop).status,project list,tab list,pane list --json,session list(cross-checked againstzmx ls— sessions from another running Macterm instance correctly show as unbound),session infohit/miss, exit-2-with-paths-tried when the app is quit, and socket unlink on quit.