fix: reliable idle/busy/waiting detection for Claude panes#15
Conversation
A Stop hook event means Claude finished its turn and handed control back to the user, which is an idle state. Previously the Stop handler ran a prose heuristic over the last assistant message and flagged turns ending in a question (or phrases like "would you like"/"should I") as waiting-input/waiting-question, so idle panes showed the waiting "!" icon. Genuine blocking prompts still arrive via dedicated structured hooks (PreToolUse+AskUserQuestion, PermissionRequest, Elicitation), so the end-of-turn text heuristic is removed. The preview fallback is likewise tightened to only infer waiting from a real multiple-choice list or permission prompt rather than a trailing question mark.
Hook state alone is unreliable for the running->idle transition because Claude does not always emit a Stop event after a tool batch, and panes with no matching hook state fell back to a detector that always reported 'running'. Both paths made idle Claude panes render as busy. The tmux pane preview is the real-time source of truth: - 'esc to interrupt' (or an active spinner status line) => running - an on-screen permission/multiple-choice prompt, or an interactive slash-command dialog (Enter to confirm / Esc to cancel) => waiting - any other readable Claude screen => idle (absence of 'esc to interrupt' means Claude is not working) Hook state is now used to enrich session/title metadata and, only within a short freshness window, to assert a structured wait (e.g. MCP elicitation) that the preview cannot render. Capture failures still fall back to hook state and then process detection.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7909ecea13
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return false; | ||
| } | ||
|
|
||
| return Date.now() - (state.updatedAt ?? 0) < CLAUDE_HOOK_WAIT_FRESHNESS_MS; |
There was a problem hiding this comment.
Keep active Claude wait hooks until they resolve
When an AskUserQuestion or Elicitation prompt stays unanswered for more than 60 seconds and the live pane preview does not expose that structured form, this freshness check makes attachRuntimeWithClaude stop honoring the hook and fall through to the preview's idle classification. Those hook events represent Claude being blocked until a follow-up response/result hook arrives, so the waiting indicator can disappear while Claude is still waiting on the user.
Useful? React with 👍 / 👎.
Problem
Claude panes were showing the wrong status:
!icon (original report).runninginstead ofidle.Root cause
Status came primarily from Claude Code hook events, which are unreliable for the busy→idle transition:
Stophandler used a prose heuristic and flagged end-of-turn messages ending in?aswaiting.Stopafter a tool batch, so the state got stuck on the lastrunning-producing event (e.g.PostToolBatch).running.Fix
Make the live tmux pane preview the authoritative real-time signal for Claude activity:
esc to interruptin the footer (or an active spinner status line) → runningEnter to confirm/Esc to cancel) → waitingesc to interrupt) → idleHook state is now used to:
waiting/runningevents from lingering.Stopevents are also classified asidleat the source, and capture failures still fall back to hook state → process detection.Verified live
All local Claude panes now report correctly: idle sessions show
idle, an actively-generating pane showsrunning, and a pane with the/effortmenu open showswaiting— all viaclaude-preview.Testing
Stopstays idle; stalerunninghook state is overridden toidleby the preview; interrupt footer →running; slash-command dialog →waiting.npm test(132 pass),npm run typecheck,npm run lintall clean.