Skip to content

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

Closed
thdxg wants to merge 1 commit into
claude/mystifying-colden-103489from
claude/mystifying-colden-103489-pr2
Closed

Add mutation verbs to the macterm CLI (create, split, run, grid)#140
thdxg wants to merge 1 commit into
claude/mystifying-colden-103489from
claude/mystifying-colden-103489-pr2

Conversation

@thdxg

@thdxg thdxg commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Second of three PRs for #107, stacked on #139 (base = its branch; will retarget to main once #139 lands). This makes the control plane actually drive the app; PR 3 wires it into the CI benchmark as a workload generator.

Verbs added

  • project create <path> [--name] [--select] — validated through ProjectPath (remote scp-specs rejected with a clear error until Remote projects over SSH backed by zmx #104 stage 2), idempotent by canonical path so scripted setups can run unconditionally. --select rides selectProject, so a matching central project file auto-applies on first open — declarative tab spawning for free.
  • project select, tab new [--run CMD], tab select, tab close [--force]
  • pane split [--direction right|down|auto] [--run CMD], pane focus (via navigateToPane: selects tab, fronts window, restores first responder), pane close [--force], pane run <command…>
  • grid <RxC> [--run CMD] — Zentty-inspired equal-cells grid (≤16), the benchmark workhorse: macterm tab new --run X && macterm grid 2x2 --run X
  • session kill, layout apply [--force], layout save

Design points

  • Two ways to run a command, deliberately distinct: --run on tab/split/grid uses the layout run: path (spawn-time initial_input — zmx runs it only on fresh create); pane run types into an existing live shell via a new GhosttyTerminalNSView.sendText (the paste path: ghostty_surface_key with .text outside a key event). A missing surface is a typed no_surface error, never a silent drop.
  • Headless callers can't click dialogs: where the UI stages a busy/destructive confirmation (requestCloseTab, destructive layout reconciles), the handler returns a typed busy error with a --force hint instead. Force takes the direct path. (Note: ghostty's busy detection tracks child processes — a nushell builtin like sleep doesn't trip it, same as the UI.)
  • Pane self-targeting: every pane's env now carries MACTERM_SESSION (merged in ensureNSView), so a bare macterm pane split inside a pane splits itself. Session names are the restart-stable address (pane UUIDs regenerate on relaunch; a reattached zmx shell's env stays correct). pane close deliberately ignores the env fallback — destroying "whatever pane I'm in" by omission is a footgun; it demands --pane/--session.
  • Model changes are minimal command-threading: SplitNode.splitting(command:), TerminalTab.split(command:)/makeGrid, Workspace.createTab(command:), plus pane-targeted AppState.splitPane(_:direction:projectID:command:) and makeGrid. makeGrid rebalances to equal cells regardless of the auto-tiling pref (a grid is an explicit request for uniformity).
  • docs/cli.md documents the grammar, targeting precedence, the wire protocol for non-CLI clients, and the security posture (same-user filesystem perms; no token auth by design, envelope leaves room).

Testing

  • mise run format/lint/test green. New coverage: handler mutation tests (create/select/new/close/split/focus/run/grid/kill/apply/save incl. all error codes) against real AppState + tempdir stores; TerminalTab.makeGrid and command-threading model tests; layout save→apply round-trip against a tempdir ProjectFileStore.
  • Verified live against a hermetic instance (benchmark-style throwaway $HOME + MACTERM_BENCHMARK_DATA_DIR): project create --selecttab new --runpane splitgrid 2x2 --run (5-pane tab), pane run into a background tab, busy-close correctly refuses on a running /bin/sleep and --force closes, layout save writes ~/.config/macterm/projects/e2e.yaml and a now-destructive apply returns busy, session kill works, and ps eww on a pane's shell shows MACTERM_SESSION + MACTERM_SOCKET injected.

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.
@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 4, 2026
@thdxg thdxg deleted the branch claude/mystifying-colden-103489 July 5, 2026 01:29
@thdxg thdxg closed this Jul 5, 2026
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