Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions Resources/shell-integration/cmux-bash-integration.bash
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
# cmux shell integration for bash

_cmux_send() {
local payload="$1"
# Cache which send tool is available to avoid repeated PATH lookups.
_CMUX_SEND_TOOL=""
_cmux_detect_send_tool() {
if command -v ncat >/dev/null 2>&1; then
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
_CMUX_SEND_TOOL=ncat
elif command -v socat >/dev/null 2>&1; then
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH" >/dev/null 2>&1
_CMUX_SEND_TOOL=socat
elif command -v nc >/dev/null 2>&1; then
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
#
# Important: macOS/BSD nc will often wait for the peer to close the socket
# after it has finished writing. cmux keeps the connection open, so
# a plain `nc -U` can hang indefinitely and leak background processes.
#
# Prefer flags that guarantee we exit after sending, and fall back to a
# short timeout so we never block sidebar updates.
if printf '%s\n' "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
:
else
printf '%s\n' "$payload" | nc -w 1 -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1 || true
fi
_CMUX_SEND_TOOL=nc
fi
}
# Detection deferred to after _cmux_fix_path (end of file).

_cmux_send() {
local payload="$1"
case "$_CMUX_SEND_TOOL" in
ncat)
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
;;
socat)
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH" >/dev/null 2>&1
;;
nc)
if printf '%s\n' "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
:
else
printf '%s\n' "$payload" | nc -w 1 -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1 || true
fi
;;
esac
}

_cmux_restore_scrollback_once() {
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
Expand Down Expand Up @@ -271,6 +280,7 @@ _cmux_clear_pr_for_panel() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
# Synchronous: must arrive before the next report_pr from the poll loop.
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new synchronous clear_pr send no longer suppresses transport stderr, so ncat errors can print into the prompt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/shell-integration/cmux-bash-integration.bash, line 284:

<comment>The new synchronous `clear_pr` send no longer suppresses transport stderr, so `ncat` errors can print into the prompt.</comment>

<file context>
@@ -280,9 +280,8 @@ _cmux_clear_pr_for_panel() {
-        _cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
-    } >/dev/null 2>&1 & disown
+    # Synchronous: must arrive before the next report_pr from the poll loop.
+    _cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
 }
 
</file context>
Suggested change
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID" >/dev/null 2>&1
Fix with Cubic

}

Expand Down Expand Up @@ -445,9 +455,10 @@ _cmux_run_pr_probe_with_timeout() {

_cmux_stop_pr_poll_loop() {
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
# Use SIGKILL directly to avoid blocking sleep in preexec.
# The poll loop is lightweight and safe to kill abruptly.
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
# Process-group kill: background jobs are process-group leaders, so
# negative PID kills the loop + all descendants (gh, sleep) without
# the synchronous /bin/ps + awk of tree-kill (~5-13ms).
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Process-group kill assumes $! is always a PGID leader; when that isn’t true, stop can fail silently and leave orphaned PR poll loops running.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/shell-integration/cmux-bash-integration.bash, line 461:

<comment>Process-group kill assumes `$!` is always a PGID leader; when that isn’t true, stop can fail silently and leave orphaned PR poll loops running.</comment>

<file context>
@@ -456,9 +455,10 @@ _cmux_run_pr_probe_with_timeout() {
+        # Process-group kill: background jobs are process-group leaders, so
+        # negative PID kills the loop + all descendants (gh, sleep) without
+        # the synchronous /bin/ps + awk of tree-kill (~5-13ms).
+        kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true
         _CMUX_PR_POLL_PID=""
     fi
</file context>
Suggested change
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || kill -KILL "$_CMUX_PR_POLL_PID" 2>/dev/null || true
Fix with Cubic

_CMUX_PR_POLL_PID=""
fi
}
Expand Down Expand Up @@ -702,4 +713,6 @@ _cmux_fix_path() {
_cmux_fix_path
unset -f _cmux_fix_path

_cmux_detect_send_tool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-resolve bash send transport after PATH changes

The transport is detected once at startup (_cmux_detect_send_tool) and then _cmux_send trusts the cached command forever. If the user later mutates PATH (common with direnv/venv/nix shells), the cached tool can disappear while another compatible tool is still present; _cmux_send then fails instead of falling back, so sidebar telemetry updates stop. The previous behavior re-checked availability per send and did not regress on post-startup PATH changes.

Useful? React with 👍 / 👎.


_cmux_install_prompt_command
64 changes: 37 additions & 27 deletions Resources/shell-integration/cmux-zsh-integration.zsh
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
# cmux shell integration for zsh
# Injected automatically — do not source manually

# Prefer zsh/net/unix for socket sends (no fork, ~0.2ms per send vs ~3ms
# for fork+exec of ncat/socat/nc). Falls back to external tools if the
# module is unavailable.
typeset -g _CMUX_HAS_ZSOCKET=0
if zmodload zsh/net/unix 2>/dev/null; then
_CMUX_HAS_ZSOCKET=1
fi

_cmux_send() {
local payload="$1"
if (( _CMUX_HAS_ZSOCKET )); then
local fd
zsocket "$CMUX_SOCKET_PATH" 2>/dev/null || return 1
fd=$REPLY
print -u $fd -r -- "$payload" 2>/dev/null
exec {fd}>&- 2>/dev/null
return 0
fi
if command -v ncat >/dev/null 2>&1; then
print -r -- "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
elif command -v socat >/dev/null 2>&1; then
print -r -- "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH" >/dev/null 2>&1
elif command -v nc >/dev/null 2>&1; then
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
#
# Important: macOS/BSD nc will often wait for the peer to close the socket
# after it has finished writing. cmux keeps the connection open, so
# a plain `nc -U` can hang indefinitely and leak background processes.
#
# Prefer flags that guarantee we exit after sending, and fall back to a
# short timeout so we never block sidebar updates.
if print -r -- "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
:
else
Expand All @@ -24,6 +32,16 @@ _cmux_send() {
fi
}

# Fire-and-forget send: synchronous when zsocket is available (fast, no fork),
# backgrounded otherwise.
_cmux_send_bg() {
if (( _CMUX_HAS_ZSOCKET )); then
_cmux_send "$1"
else
{ _cmux_send "$1" } >/dev/null 2>&1 &!
fi
}

_cmux_restore_scrollback_once() {
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
[[ -n "$path" ]] || return 0
Expand Down Expand Up @@ -337,9 +355,7 @@ _cmux_report_tty_once() {
[[ -n "$payload" ]] || return 0

_CMUX_TTY_REPORTED=1
{
_cmux_send "$payload"
} >/dev/null 2>&1 &!
_cmux_send_bg "$payload"
}

_cmux_report_shell_activity_state() {
Expand All @@ -350,9 +366,7 @@ _cmux_report_shell_activity_state() {
[[ -n "$CMUX_PANEL_ID" ]] || return 0
[[ "$_CMUX_SHELL_ACTIVITY_LAST" == "$state" ]] && return 0
_CMUX_SHELL_ACTIVITY_LAST="$state"
{
_cmux_send "report_shell_state $state --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &!
_cmux_send_bg "report_shell_state $state --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}

_cmux_ports_kick() {
Expand All @@ -362,9 +376,7 @@ _cmux_ports_kick() {
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_CMUX_PORTS_LAST_RUN=$EPOCHSECONDS
{
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &!
_cmux_send_bg "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}

_cmux_report_git_branch_for_path() {
Expand Down Expand Up @@ -392,7 +404,7 @@ _cmux_clear_pr_for_panel() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
_cmux_send_bg "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep zsh clear_pr ordered before restarting PR polling

_cmux_clear_pr_for_panel now uses _cmux_send_bg, and in the fallback path (_CMUX_HAS_ZSOCKET=0) that send is detached. In _cmux_precmd, this clear runs immediately before _cmux_start_pr_poll_loop, so a delayed clear_pr can arrive after a fresh report_pr from the restarted poller and wipe a correct badge until the next poll cycle. This is a user-visible correctness regression on systems where zsh/net/unix is unavailable.

Useful? React with 👍 / 👎.

}

_cmux_pr_output_indicates_no_pull_request() {
Expand Down Expand Up @@ -567,9 +579,10 @@ _cmux_run_pr_probe_with_timeout() {

_cmux_stop_pr_poll_loop() {
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
# Use SIGKILL directly to avoid blocking sleep in preexec.
# The poll loop is lightweight and safe to kill abruptly.
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
# Process-group kill: background jobs are process-group leaders, so
# negative PID kills the loop + all descendants (gh, sleep) without
# the synchronous /bin/ps + awk of tree-kill (~5-13ms).
kill -KILL -- -"$_CMUX_PR_POLL_PID" 2>/dev/null || true
_CMUX_PR_POLL_PID=""
fi
}
Comment on lines 580 to 588

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Orphaned gh processes may accumulate across rapid commands

kill -KILL $_CMUX_PR_POLL_PID kills only the top-level poll loop process. The subshell chain it spawned—_cmux_run_pr_probe_with_timeout → _cmux_report_pr_for_path → gh pr view—is reparented to launchd and continues running until the network call completes (up to _CMUX_ASYNC_JOB_TIMEOUT = 20 s).

In normal interactive use this is fine (one gh call per 45 s poll, single orphan lives for ≤ 5 s). However, in rapid-fire scenarios—running many commands in quick succession, or an agent loop executing commands at < 5 s intervals—each preexec call to _cmux_stop_pr_poll_loop leaves a new orphaned gh pr view behind while the previous one is still in flight. This can stack up multiple simultaneous gh API calls, risk GitHub's per-minute rate limit, and add unnecessary background CPU/network pressure.

The same change exists in the bash integration at line 461.

If the trade-off is intentional (it is explicitly called out in the PR description), consider at minimum capping the orphan window by signaling the intermediate subshell group. For example, if _CMUX_PR_POLL_PID is set as a process group leader, kill -KILL -- -$_CMUX_PR_POLL_PID kills the entire group without the ps+awk cost.

Expand Down Expand Up @@ -682,7 +695,7 @@ _cmux_precmd() {
_cmux_report_shell_activity_state prompt

# Handle cases where Ghostty integration initializes after this file.
_cmux_patch_ghostty_semantic_redraw
(( _CMUX_GHOSTTY_SEMANTIC_PATCHED )) || _cmux_patch_ghostty_semantic_redraw

if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
Expand Down Expand Up @@ -717,11 +730,8 @@ _cmux_precmd() {
# This is also the simplest way to test sidebar directory behavior end-to-end.
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
_CMUX_PWD_LAST_PWD="$pwd"
{
# Quote to preserve spaces.
local qpwd="${pwd//\"/\\\"}"
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
} >/dev/null 2>&1 &!
local qpwd="${pwd//\"/\\\"}"
_cmux_send_bg "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
fi

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