Skip to content

Commit 314e1df

Browse files
authored
refactor: shell integration speaks v2 JSON-RPC on the direct socket path (#72)
* fix: mismatched brace/paren in _cmux_start_pr_poll_loop broke bash sourcing _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. * refactor: shell integration speaks v2 JSON-RPC on the direct socket path 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.
1 parent 816d154 commit 314e1df

2 files changed

Lines changed: 109 additions & 35 deletions

File tree

Resources/shell-integration/programa-bash-integration.bash

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ _cmux_json_escape() {
6666
printf '%s\n' "$value"
6767
}
6868

69+
# Build a single-line v2 JSON-RPC request frame for the direct-socket
70+
# (fire-and-forget) path. `params_json` must already be a well-formed JSON
71+
# object string (see call sites, which use _cmux_json_escape on any
72+
# user-controlled values before interpolating them).
73+
_cmux_json_rpc_frame() {
74+
local method="$1"
75+
local params_json="$2"
76+
printf '%s\n' "{\"id\":1,\"method\":\"$method\",\"params\":$params_json}"
77+
}
78+
6979
_cmux_relay_rpc_bg() {
7080
local method="$1"
7181
local params="$2"
@@ -358,13 +368,17 @@ _cmux_report_tty_payload() {
358368
[[ -n "$PROGRAMA_TAB_ID" ]] || return 0
359369
[[ -n "$_PROGRAMA_TTY_NAME" ]] || return 0
360370

361-
local payload="report_tty $_PROGRAMA_TTY_NAME --tab=$PROGRAMA_TAB_ID"
371+
local workspace_id="" tty_name_json params
372+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
373+
tty_name_json="$(_cmux_json_escape "$_PROGRAMA_TTY_NAME")"
374+
params="{\"workspace_id\":\"$workspace_id\",\"tty_name\":\"$tty_name_json\""
362375
if [[ -z "$TMUX" ]]; then
363376
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
364-
payload+=" --panel=$PROGRAMA_PANEL_ID"
377+
params+=",\"surface_id\":\"$PROGRAMA_PANEL_ID\""
365378
fi
379+
params+="}"
366380

367-
printf '%s\n' "$payload"
381+
_cmux_json_rpc_frame "surface.report_tty" "$params"
368382
}
369383

370384
_cmux_report_tty_once() {
@@ -398,8 +412,12 @@ _cmux_report_shell_activity_state() {
398412
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
399413
[[ "$_PROGRAMA_SHELL_ACTIVITY_LAST" == "$state" ]] && return 0
400414
_PROGRAMA_SHELL_ACTIVITY_LAST="$state"
415+
local workspace_id="" state_json params
416+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
417+
state_json="$(_cmux_json_escape "$state")"
418+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"state\":\"$state_json\"}"
401419
(
402-
_cmux_send "report_shell_state $state --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
420+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_shell_state" "$params")"
403421
) >/dev/null 2>&1 &
404422
disown 2>/dev/null
405423
}
@@ -415,8 +433,12 @@ _cmux_ports_kick() {
415433
fi
416434
_PROGRAMA_PORTS_LAST_RUN="$(_cmux_now)"
417435
if _cmux_socket_is_unix; then
436+
local workspace_id="" reason_json params
437+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
438+
reason_json="$(_cmux_json_escape "$reason")"
439+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"reason\":\"$reason_json\"}"
418440
{
419-
_cmux_send "ports_kick --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID --reason=$reason"
441+
_cmux_send "$(_cmux_json_rpc_frame "surface.ports_kick" "$params")"
420442
} >/dev/null 2>&1 & disown
421443
else
422444
_cmux_ports_kick_via_relay "$reason"
@@ -427,8 +449,11 @@ _cmux_clear_pr_for_panel() {
427449
[[ -S "$PROGRAMA_SOCKET_PATH" ]] || return 0
428450
[[ -n "$PROGRAMA_TAB_ID" ]] || return 0
429451
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
452+
local workspace_id="" params
453+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
454+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\"}"
430455
# Synchronous: must arrive before the next report_pr from the poll loop.
431-
_cmux_send "clear_pr --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
456+
_cmux_send "$(_cmux_json_rpc_frame "surface.clear_pr" "$params")"
432457
}
433458

434459
_cmux_pr_output_indicates_no_pull_request() {
@@ -629,9 +654,9 @@ _cmux_report_pr_for_path() {
629654
fi
630655

631656
case "$state" in
632-
MERGED) status_opt="--state=merged" ;;
633-
OPEN) status_opt="--state=open" ;;
634-
CLOSED) status_opt="--state=closed" ;;
657+
MERGED) status_opt="merged" ;;
658+
OPEN) status_opt="open" ;;
659+
CLOSED) status_opt="closed" ;;
635660
*) return 1 ;;
636661
esac
637662

@@ -645,8 +670,12 @@ _cmux_report_pr_for_path() {
645670
_PROGRAMA_PR_LAST_BRANCH="$branch"
646671
_PROGRAMA_PR_NO_PR_BRANCH=""
647672

648-
local quoted_branch="${branch//\"/\\\"}"
649-
_cmux_send "report_pr $number $url $status_opt --branch=\"$quoted_branch\" --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
673+
local workspace_id="" branch_json url_json params
674+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
675+
branch_json="$(_cmux_json_escape "$branch")"
676+
url_json="$(_cmux_json_escape "$url")"
677+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"number\":$number,\"url\":\"$url_json\",\"state\":\"$status_opt\",\"branch\":\"$branch_json\"}"
678+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_pr" "$params")"
650679
}
651680

652681
_cmux_child_pids() {
@@ -745,7 +774,7 @@ _cmux_start_pr_poll_loop() {
745774
fi
746775
_PROGRAMA_PR_POLL_PWD="$watch_pwd"
747776

748-
{
777+
(
749778
local signal_path=""
750779
signal_path="$(_cmux_pr_force_signal_path 2>/dev/null || true)"
751780
while :; do
@@ -913,8 +942,11 @@ _cmux_prompt_command() {
913942
if [[ "$pwd" != "$_PROGRAMA_PWD_LAST_PWD" ]]; then
914943
_PROGRAMA_PWD_LAST_PWD="$pwd"
915944
(
916-
local qpwd="${pwd//\"/\\\"}"
917-
_cmux_send "report_pwd \"${qpwd}\" --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
945+
local workspace_id="" pwd_json params
946+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
947+
pwd_json="$(_cmux_json_escape "$pwd")"
948+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"path\":\"$pwd_json\"}"
949+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_pwd" "$params")"
918950
) >/dev/null 2>&1 &
919951
disown 2>/dev/null
920952
fi
@@ -963,15 +995,20 @@ _cmux_prompt_command() {
963995
(
964996
# Skip git operations if not in a git repository to avoid TCC prompts
965997
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
966-
local branch dirty_opt=""
998+
local branch dirty=false workspace_id="" params
967999
branch=$(git branch --show-current 2>/dev/null)
1000+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
9681001
if [[ -n "$branch" ]]; then
9691002
local first
9701003
first=$(git status --porcelain -uno 2>/dev/null | head -1)
971-
[[ -n "$first" ]] && dirty_opt="--status=dirty"
972-
_cmux_send "report_git_branch $branch $dirty_opt --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
1004+
[[ -n "$first" ]] && dirty=true
1005+
local branch_json
1006+
branch_json="$(_cmux_json_escape "$branch")"
1007+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"branch\":\"$branch_json\",\"dirty\":$dirty}"
1008+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_git_branch" "$params")"
9731009
else
974-
_cmux_send "clear_git_branch --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
1010+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\"}"
1011+
_cmux_send "$(_cmux_json_rpc_frame "surface.clear_git_branch" "$params")"
9751012
fi
9761013
) >/dev/null 2>&1 &
9771014
_PROGRAMA_GIT_JOB_PID=$!

Resources/shell-integration/programa-zsh-integration.zsh

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ _cmux_json_escape() {
7676
print -r -- "$value"
7777
}
7878

79+
# Build a single-line v2 JSON-RPC request frame for the direct-socket
80+
# (fire-and-forget) path. `params_json` must already be a well-formed JSON
81+
# object string (see call sites, which use _cmux_json_escape on any
82+
# user-controlled values before interpolating them).
83+
_cmux_json_rpc_frame() {
84+
local method="$1"
85+
local params_json="$2"
86+
print -r -- "{\"id\":1,\"method\":\"$method\",\"params\":$params_json}"
87+
}
88+
7989
_cmux_relay_rpc_bg() {
8090
local method="$1"
8191
local params="$2"
@@ -458,13 +468,17 @@ _cmux_report_tty_payload() {
458468
[[ -n "$PROGRAMA_TAB_ID" ]] || return 0
459469
[[ -n "$_PROGRAMA_TTY_NAME" ]] || return 0
460470

461-
local payload="report_tty $_PROGRAMA_TTY_NAME --tab=$PROGRAMA_TAB_ID"
471+
local workspace_id="" tty_name_json params
472+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
473+
tty_name_json="$(_cmux_json_escape "$_PROGRAMA_TTY_NAME")"
474+
params="{\"workspace_id\":\"$workspace_id\",\"tty_name\":\"$tty_name_json\""
462475
if [[ -z "$TMUX" ]]; then
463476
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
464-
payload+=" --panel=$PROGRAMA_PANEL_ID"
477+
params+=",\"surface_id\":\"$PROGRAMA_PANEL_ID\""
465478
fi
479+
params+="}"
466480

467-
print -r -- "$payload"
481+
_cmux_json_rpc_frame "surface.report_tty" "$params"
468482
}
469483

470484
_cmux_report_tty_once() {
@@ -496,7 +510,11 @@ _cmux_report_shell_activity_state() {
496510
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
497511
[[ "$_PROGRAMA_SHELL_ACTIVITY_LAST" == "$state" ]] && return 0
498512
_PROGRAMA_SHELL_ACTIVITY_LAST="$state"
499-
_cmux_send_bg "report_shell_state $state --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
513+
local workspace_id="" state_json params
514+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
515+
state_json="$(_cmux_json_escape "$state")"
516+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"state\":\"$state_json\"}"
517+
_cmux_send_bg "$(_cmux_json_rpc_frame "surface.report_shell_state" "$params")"
500518
}
501519

502520
_cmux_ports_kick() {
@@ -510,7 +528,11 @@ _cmux_ports_kick() {
510528
fi
511529
_PROGRAMA_PORTS_LAST_RUN="$(_cmux_now)"
512530
if _cmux_socket_is_unix; then
513-
_cmux_send_bg "ports_kick --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID --reason=$reason"
531+
local workspace_id="" reason_json params
532+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
533+
reason_json="$(_cmux_json_escape "$reason")"
534+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"reason\":\"$reason_json\"}"
535+
_cmux_send_bg "$(_cmux_json_rpc_frame "surface.ports_kick" "$params")"
514536
else
515537
_cmux_ports_kick_via_relay "$reason"
516538
fi
@@ -526,22 +548,30 @@ _cmux_report_git_branch_for_path() {
526548
# Skip git operations if not in a git repository to avoid TCC prompts
527549
git -C "$repo_path" rev-parse --git-dir >/dev/null 2>&1 || return 0
528550

529-
local branch dirty_opt="" first
551+
local branch dirty=false first workspace_id="" params
530552
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
553+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
531554
if [[ -n "$branch" ]]; then
532555
first="$(git -C "$repo_path" status --porcelain -uno 2>/dev/null | head -1)"
533-
[[ -n "$first" ]] && dirty_opt="--status=dirty"
534-
_cmux_send "report_git_branch $branch $dirty_opt --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
556+
[[ -n "$first" ]] && dirty=true
557+
local branch_json
558+
branch_json="$(_cmux_json_escape "$branch")"
559+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"branch\":\"$branch_json\",\"dirty\":$dirty}"
560+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_git_branch" "$params")"
535561
else
536-
_cmux_send "clear_git_branch --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
562+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\"}"
563+
_cmux_send "$(_cmux_json_rpc_frame "surface.clear_git_branch" "$params")"
537564
fi
538565
}
539566

540567
_cmux_clear_pr_for_panel() {
541568
[[ -S "$PROGRAMA_SOCKET_PATH" ]] || return 0
542569
[[ -n "$PROGRAMA_TAB_ID" ]] || return 0
543570
[[ -n "$PROGRAMA_PANEL_ID" ]] || return 0
544-
_cmux_send_bg "clear_pr --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
571+
local workspace_id="" params
572+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
573+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\"}"
574+
_cmux_send_bg "$(_cmux_json_rpc_frame "surface.clear_pr" "$params")"
545575
}
546576

547577
_cmux_pr_output_indicates_no_pull_request() {
@@ -742,9 +772,9 @@ _cmux_report_pr_for_path() {
742772
fi
743773

744774
case "$state" in
745-
MERGED) status_opt="--state=merged" ;;
746-
OPEN) status_opt="--state=open" ;;
747-
CLOSED) status_opt="--state=closed" ;;
775+
MERGED) status_opt="merged" ;;
776+
OPEN) status_opt="open" ;;
777+
CLOSED) status_opt="closed" ;;
748778
*) return 1 ;;
749779
esac
750780

@@ -758,8 +788,12 @@ _cmux_report_pr_for_path() {
758788
_PROGRAMA_PR_LAST_BRANCH="$branch"
759789
_PROGRAMA_PR_NO_PR_BRANCH=""
760790

761-
local quoted_branch="${branch//\"/\\\"}"
762-
_cmux_send "report_pr $number $url $status_opt --branch=\"$quoted_branch\" --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
791+
local workspace_id="" branch_json url_json params
792+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
793+
branch_json="$(_cmux_json_escape "$branch")"
794+
url_json="$(_cmux_json_escape "$url")"
795+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"number\":$number,\"url\":\"$url_json\",\"state\":\"$status_opt\",\"branch\":\"$branch_json\"}"
796+
_cmux_send "$(_cmux_json_rpc_frame "surface.report_pr" "$params")"
763797
}
764798

765799
_cmux_child_pids() {
@@ -1074,8 +1108,11 @@ _cmux_precmd() {
10741108
# This is also the simplest way to test sidebar directory behavior end-to-end.
10751109
if [[ "$pwd" != "$_PROGRAMA_PWD_LAST_PWD" ]]; then
10761110
_PROGRAMA_PWD_LAST_PWD="$pwd"
1077-
local qpwd="${pwd//\"/\\\"}"
1078-
_cmux_send_bg "report_pwd \"${qpwd}\" --tab=$PROGRAMA_TAB_ID --panel=$PROGRAMA_PANEL_ID"
1111+
local workspace_id="" pwd_json params
1112+
workspace_id="$(_cmux_relay_workspace_id)" || workspace_id="$PROGRAMA_TAB_ID"
1113+
pwd_json="$(_cmux_json_escape "$pwd")"
1114+
params="{\"workspace_id\":\"$workspace_id\",\"surface_id\":\"$PROGRAMA_PANEL_ID\",\"path\":\"$pwd_json\"}"
1115+
_cmux_send_bg "$(_cmux_json_rpc_frame "surface.report_pwd" "$params")"
10791116
fi
10801117

10811118
# Git branch/dirty: update immediately on directory change, otherwise every ~3s.

0 commit comments

Comments
 (0)