Skip to content

refactor: shell integration speaks v2 JSON-RPC on the direct socket path#72

Merged
arzafran merged 2 commits into
mainfrom
refactor/shell-integration-v2
Jul 8, 2026
Merged

refactor: shell integration speaks v2 JSON-RPC on the direct socket path#72
arzafran merged 2 commits into
mainfrom
refactor/shell-integration-v2

Conversation

@arzafran

@arzafran arzafran commented Jul 8, 2026

Copy link
Copy Markdown
Member

What this does

Step 3 of the v1-deletion sequence. The zsh and bash shell-integration scripts (the code injected into every terminal session) talk to the app over a Unix socket to report things like the current directory, git branch, and open PR — until now that direct-socket path sent old-style plain-text lines (report_pwd "/some/path" --tab=X --panel=Y). This switches it to the same JSON-RPC protocol the REMOTE-relay path in the same scripts already speaks, so both transports use one protocol and v1 can eventually be deleted from the app.

Along the way, verifying with bash -n turned up a pre-existing, unrelated bug: _cmux_start_pr_poll_loop in the bash script opened a { ... } block but closed it with ) (a typo from March 2026). That mismatch makes bash fail to parse the entire script, so sourcing programa-bash-integration.bash currently errors out and none of its shell-integration functions get defined at all — confirmed on unmodified origin/main in both bash 3.2 and 5.3. Fixed as its own commit ahead of the JSON-RPC change.

Summary

  • Added _cmux_json_rpc_frame() to each script — builds a single-line {"id":1,"method":...,"params":{...}} frame.
  • Converted the 8 direct-socket verbs to their v2 equivalents (params per Sources/TerminalController.swift's v2 dispatch):
    • report_tty -> surface.report_tty {workspace_id, tty_name, surface_id?} (surface_id omitted under tmux, matching v1)
    • report_shell_state -> surface.report_shell_state {workspace_id, surface_id, state}
    • report_pwd -> surface.report_pwd {workspace_id, surface_id, path}
    • report_git_branch -> surface.report_git_branch {workspace_id, surface_id, branch, dirty}
    • clear_git_branch -> surface.clear_git_branch {workspace_id, surface_id}
    • report_pr -> surface.report_pr {workspace_id, surface_id, number, url, state, branch}
    • clear_pr -> surface.clear_pr {workspace_id, surface_id}
    • ports_kick -> surface.ports_kick {workspace_id, surface_id?, reason}
  • workspace_id/surface_id are sourced from the existing PROGRAMA_WORKSPACE_ID/PROGRAMA_TAB_ID/PROGRAMA_PANEL_ID env vars via the already-present _cmux_relay_workspace_id helper — the same identifiers the v1 fast path and the v2 relay path already use.
  • Reused the existing _cmux_json_escape helper (already used by the relay path) rather than adding a new one, so pwd paths / branch names with quotes, backslashes, or unicode are escaped consistently everywhere.
  • Fire-and-forget semantics unchanged: _cmux_send/_cmux_send_bg never read a response before this change and still don't; call sites that were synchronous vs. backgrounded stay that way.
  • Relay path (_cmux_relay_rpc, _cmux_report_tty_via_relay, etc.) is untouched.
  • Both scripts were changed in lockstep to stay behaviorally identical.

Test Plan

  • zsh -n Resources/shell-integration/programa-zsh-integration.zsh
  • bash -n Resources/shell-integration/programa-bash-integration.bash (on both commits individually, and against unmodified origin/main to confirm the brace bug predates this PR)
  • Sourced both scripts in a live shell (bash 3.2 + 5.3, zsh) to confirm they load without error
  • Validated generated JSON-RPC frames with python3 -m json.tool, including a nasty path with an apostrophe, embedded quotes, a literal backslash, and unicode (/tmp/it's "here"\there/ünïcödé/日本語) — round-trips correctly for both the zsh and bash escape helpers
  • Cannot integration-test against a live app from this environment (repo policy). The real gate is the CI shell-integration Python suite (tests/) and a manual smoke test of shell integration (open a terminal, cd around, check a git branch, open a PR) — recommended before the final v1 removal PR.

arzafran added 2 commits July 8, 2026 09:17
…urcing

_cmux_start_pr_poll_loop opened its backgrounded PR-poll loop with a bare
`{` but closed it with `)` (typo introduced in 88d6a2f, March 2026).
A `{ ... }` group must close with `}`; mixing it with `)` is a syntax
error, and bash apparently fails the syntax check for the entire file, not
just the malformed function — sourcing programa-bash-integration.bash in a
live shell (bash 3.2 and 5.3 both tested) aborts entirely with 'syntax
error near unexpected token )', so no shell-integration function gets
defined at all.

Found while running `bash -n` as part of verifying an unrelated change
(the v1->v2 JSON-RPC migration in the following commits); confirmed
present on unmodified origin/main via `git show HEAD`. Fixed by opening
a subshell with `(` instead, matching the `( ... ) &` backgrounding
pattern already used elsewhere in this same file (e.g.
_cmux_run_pr_probe_with_timeout, _cmux_prompt_command's pwd/git-branch
probes) — subshells support $! for the backgrounded PID same as job
groups do.
Step 3 of the v1-deletion sequence (see docs/v2-api-migration.md). Converts
the direct-socket sends in the zsh and bash shell-integration scripts from
v1 space-delimited lines to single-line v2 JSON-RPC frames, mirroring the
pattern the REMOTE-relay path in the same scripts already uses
(_cmux_relay_rpc "surface.report_tty" etc., added in PR #70's v2 parity
methods, commit 475aa2f).

Verbs converted (both scripts, kept behaviorally identical to each other):
- report_tty       -> surface.report_tty       {workspace_id, tty_name, surface_id?}
- report_shell_state -> surface.report_shell_state {workspace_id, surface_id, state}
- report_pwd       -> surface.report_pwd       {workspace_id, surface_id, path}
- report_git_branch -> surface.report_git_branch {workspace_id, surface_id, branch, dirty}
- clear_git_branch -> surface.clear_git_branch {workspace_id, surface_id}
- report_pr        -> surface.report_pr        {workspace_id, surface_id, number, url, state, branch}
- clear_pr         -> surface.clear_pr         {workspace_id, surface_id}
- ports_kick       -> surface.ports_kick       {workspace_id, surface_id?, reason}

workspace_id/surface_id are sourced from the same env vars the v1 fast path
and the v2 relay path already use (PROGRAMA_WORKSPACE_ID falling back to
PROGRAMA_TAB_ID, and PROGRAMA_PANEL_ID) via the existing _cmux_relay_workspace_id
helper. report_tty preserves the v1 tmux special case of omitting surface_id.

Both scripts reuse the existing _cmux_json_escape helper (already used by the
relay path) to safely escape quotes/backslashes/control chars in pwd paths and
branch names before interpolating them into the JSON frames.

Fire-and-forget semantics are unchanged: _cmux_send/_cmux_send_bg never read a
response today and still don't after this change; call sites that were
synchronous (report_git_branch/clear_git_branch/report_pr, which already run
inside an already-backgrounded subshell) stay synchronous, and call sites that
were backgrounded stay backgrounded.

The relay path itself is untouched.
@arzafran arzafran merged commit 314e1df into main Jul 8, 2026
8 checks passed
arzafran added a commit that referenced this pull request Jul 8, 2026
v2 JSON-RPC is now the only socket protocol. All consumers were migrated
first (CLI in #74, shell integration in #72, tests in #71/#73; the remote
daemon was already v2-only), so this deletes the v1 line dispatch itself
and everything that only existed to serve it.

- TerminalController.processCommand now dispatches JSON unconditionally;
  a non-JSON line gets a terse v1_removed error instead of being routed
  to the old space-delimited command switch.
- Deletes the v1 switch (~90 cases) and every handler function that
  becomes unreferenced, plus their v1-only parsing helpers
  (parseOptions/parseOptionsNoStop, resolveTabForReport,
  withReportTargetSurface, upsertSidebarMetadata/clearSidebarMetadata/
  listSidebarMetadata, schedulePanelMetadataMutation,
  parseReadScreenArgs). Debug handlers that v2's debug.* methods reuse
  as their implementation core (setShortcut, simulateShortcut,
  activateApp, isTerminalFocused, readTerminalText, renderStats,
  layoutDebug, bonsplit/empty-panel/flash counters,
  focusFromNotification, panelSnapshot(Reset), captureScreenshot) are
  kept, since they're still live v2 entry points, not v1 relics.
  Self.socketFastPathState and tailTerminalLines/
  readTerminalTextBase64(terminalPanel:) stay too - v2 surface-telemetry
  and screen-read methods depend on them directly.
- auth was already a connection-level preamble (authResponseIfNeeded,
  handled before any protocol dispatch) supporting both a raw
  auth-plus-password line and v2 auth.login; only the CLI's raw-line
  call needed migrating, which this does (auth.login now).
- Removes the now-unreachable isV2:false focus-intent allowlist
  (focusIntentV1Commands) and updates the one unit test that exercised
  it directly to cover the v2 equivalent instead.
- Docs: v2-api-migration.md collapses to a short removal note plus the
  v1->v2 name mapping (kept for reference); socket-focus-steal-audit.todo.md
  and agent-browser-port-spec.md get historical annotations; CLAUDE.md's
  socket-threading-policy examples are reworded to v2 method names.

TerminalController.swift: 12834 -> 9615 lines (-3219). Net repo diff:
+156/-3410 across 7 files.

Breaking change: any external script or tool that still speaks the old
space-delimited socket protocol directly (rather than through the CLI)
will now get a v1_removed JSON error instead of a response. Use the CLI
or the v2 JSON-RPC protocol described in docs/v2-api-migration.md.
@arzafran arzafran deleted the refactor/shell-integration-v2 branch July 10, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant