* Fix stale Claude status in sidebar by adding missing hooks and OSC suppression The Claude Code integration only used 3 hooks (SessionStart, Stop, Notification), leaving gaps that caused stale sidebar status. Now uses 6 hooks: - SessionEnd: clears status when Claude exits (covers Ctrl+C where Stop doesn't fire) - UserPromptSubmit: clears "Needs input" and sets "Running" on new prompt - PreToolUse (async): clears "Needs input" when Claude resumes after permission grant Also: - Suppress OSC 9/99 desktop notifications for workspaces with active Claude hook sessions to prevent duplicates from the raw OSC path - Store Claude process PID in status entries for stale-session detection - Add 30-second sweep timer that checks agent PIDs and clears stale entries (safety net for SIGKILL/crash where no hook fires) - Update wrapper test expectations for the new hook set Fixes https://github.com/manaflow-ai/cmux/issues/1301 * Don't show "Running" status on Claude launch, only when actually working SessionStart now registers the PID for tracking and OSC suppression via set_agent_pid without setting a visible status entry. "Running" only appears when the user submits a prompt (UserPromptSubmit) or Claude starts using tools (PreToolUse). Added set_agent_pid / clear_agent_pid socket commands to decouple PID tracking from visible status entries. OSC suppression checks agentPIDs instead of statusEntries so it works during the initial idle period. * Don't restore status entries across app restarts Status entries are ephemeral runtime state tied to running processes (e.g. claude_code "Running"). Restoring them after restart shows stale status for processes that no longer exist. * Address PR review comments and remove debug logging - session-end: only clear status/PID/notifications when Stop didn't fire first - PID sweep: check errno == ESRCH instead of treating all kill(pid,0) failures as dead - Validate CMUX_CLAUDE_PID > 0 - Propagate tracked PID in pre-tool-use setClaudeStatus - OSC suppression: use tabManagerFor(tabId:) for multi-window support - clearAgentPID: resolve tab UUID before async dispatch - restoreSessionSnapshot: also clear agentPIDs alongside statusEntries - Fix AskUserQuestion surfaceId overwrite (wrong workspace notification) - Fix notification text matching for "Claude Code needs your attention" - AskUserQuestion: render option labels as bracketed inline text - Remove artificial text truncation limits - Remove temporary JSONL debug logging from all handlers * Use resolveTabIdForSidebarMutation in clearAgentPID
96 lines
4.2 KiB
Bash
Executable file
96 lines
4.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# cmux claude wrapper - injects hooks and session tracking
|
|
#
|
|
# When running inside a cmux terminal (CMUX_SURFACE_ID is set), this wrapper
|
|
# intercepts `claude` invocations to inject --session-id and --settings flags
|
|
# so that Claude Code hooks fire back into cmux for notifications/status.
|
|
# Outside cmux, it passes through to the real claude binary unchanged.
|
|
|
|
# Find the real claude binary, skipping our own directory.
|
|
find_real_claude() {
|
|
local self_dir
|
|
self_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
local IFS=:
|
|
for d in $PATH; do
|
|
[[ "$d" == "$self_dir" ]] && continue
|
|
[[ -x "$d/claude" ]] && printf '%s' "$d/claude" && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Return 0 only when CMUX_SOCKET_PATH points to a live cmux socket.
|
|
cmux_socket_available() {
|
|
local socket="${CMUX_SOCKET_PATH:-}"
|
|
[[ -n "$socket" && -S "$socket" ]] || return 1
|
|
|
|
local self_dir cmux_bin
|
|
self_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
cmux_bin="$self_dir/cmux"
|
|
[[ -x "$cmux_bin" ]] || cmux_bin="$(command -v cmux || true)"
|
|
[[ -n "$cmux_bin" ]] || return 1
|
|
|
|
# Keep stale/hung socket checks bounded so claude startup does not block
|
|
# behind the CLI default timeout (15s).
|
|
CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.75 \
|
|
"$cmux_bin" --socket "$socket" ping >/dev/null 2>&1
|
|
}
|
|
|
|
# Pass through if not in a cmux terminal, hooks are disabled, or the cmux
|
|
# socket is unavailable (stale env / app not running).
|
|
IN_CMUX=0
|
|
if [[ -n "$CMUX_SURFACE_ID" ]]; then
|
|
IN_CMUX=1
|
|
fi
|
|
|
|
if [[ "$IN_CMUX" == "0" || "$CMUX_CLAUDE_HOOKS_DISABLED" == "1" ]] || ! cmux_socket_available; then
|
|
# In cmux-launched shells, preserve old behavior and always clear nested
|
|
# Claude session markers, even when we must pass through due to stale socket.
|
|
if [[ "$IN_CMUX" == "1" ]]; then
|
|
unset CLAUDECODE
|
|
fi
|
|
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }
|
|
exec "$REAL_CLAUDE" "$@"
|
|
fi
|
|
|
|
# Find real claude.
|
|
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }
|
|
|
|
# Pass through subcommands that don't support session/hook flags.
|
|
case "${1:-}" in
|
|
mcp|config|api-key) exec "$REAL_CLAUDE" "$@" ;;
|
|
esac
|
|
|
|
# Unset CLAUDECODE to avoid "nested session" detection — cmux terminals are
|
|
# independent sessions even when the parent shell was launched from Claude Code.
|
|
unset CLAUDECODE
|
|
|
|
# Check if the user already specified a session/resume flag.
|
|
# If so, don't inject our own --session-id (it would conflict).
|
|
SKIP_SESSION_ID=false
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--resume|--resume=*|--session-id|--session-id=*|--continue|-c)
|
|
SKIP_SESSION_ID=true
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Export the wrapper's PID. Because we exec claude below, this PID becomes
|
|
# the actual claude process PID, which hooks use for stale-session detection.
|
|
export CMUX_CLAUDE_PID=$$
|
|
|
|
# Build hooks settings JSON.
|
|
# Claude Code merges --settings additively with the user's own settings.json.
|
|
# - SessionStart/Stop/Notification: existing lifecycle hooks
|
|
# - SessionEnd: cleanup when Claude exits (covers Ctrl+C where Stop doesn't fire)
|
|
# - UserPromptSubmit: clears "Needs input" and sets "Running" on new prompt
|
|
# - PreToolUse: clears "Needs input" when Claude resumes after permission grant (async to avoid latency)
|
|
HOOKS_JSON='{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-start","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook stop","timeout":10}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-end","timeout":1}]}],"Notification":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook notification","timeout":10}]}],"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook prompt-submit","timeout":10}]}],"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook pre-tool-use","timeout":5,"async":true}]}]}}'
|
|
|
|
if [[ "$SKIP_SESSION_ID" == true ]]; then
|
|
exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"
|
|
else
|
|
SESSION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
|
exec "$REAL_CLAUDE" --session-id "$SESSION_ID" --settings "$HOOKS_JSON" "$@"
|
|
fi
|