diff --git a/Resources/shell-integration/cmux-bash-integration.bash b/Resources/shell-integration/cmux-bash-integration.bash index db9ae9f3..1d54c670 100644 --- a/Resources/shell-integration/cmux-bash-integration.bash +++ b/Resources/shell-integration/cmux-bash-integration.bash @@ -1,26 +1,35 @@ # cmux shell integration for bash +# 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 + _CMUX_SEND_TOOL=ncat + elif command -v socat >/dev/null 2>&1; then + _CMUX_SEND_TOOL=socat + elif command -v nc >/dev/null 2>&1; then + _CMUX_SEND_TOOL=nc + fi +} +# Detection deferred to after _cmux_fix_path (end of file). + _cmux_send() { local payload="$1" - if command -v ncat >/dev/null 2>&1; then - printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only - 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 - 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 - fi + 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() { @@ -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" } @@ -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 _CMUX_PR_POLL_PID="" fi } @@ -702,4 +713,6 @@ _cmux_fix_path() { _cmux_fix_path unset -f _cmux_fix_path +_cmux_detect_send_tool + _cmux_install_prompt_command diff --git a/Resources/shell-integration/cmux-zsh-integration.zsh b/Resources/shell-integration/cmux-zsh-integration.zsh index 1364f4a6..273d062c 100644 --- a/Resources/shell-integration/cmux-zsh-integration.zsh +++ b/Resources/shell-integration/cmux-zsh-integration.zsh @@ -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 @@ -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 @@ -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() { @@ -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() { @@ -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() { @@ -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" } _cmux_pr_output_indicates_no_pull_request() { @@ -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 } @@ -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 @@ -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.