Skip to content

Surface the TUI's blocking dialogs in chat, and let them be answered there#319

Open
xerhab wants to merge 1 commit into
mainfrom
pane-blocking-prompts
Open

Surface the TUI's blocking dialogs in chat, and let them be answered there#319
xerhab wants to merge 1 commit into
mainfrom
pane-blocking-prompts

Conversation

@xerhab

@xerhab xerhab commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Second follow-up to #317 — the remaining routine reason to switch to terminal mode.

The problem

Claude Code blocks a turn on two dialogs the chat could not see, because neither has a hook to intercept it and neither writes anything to the transcript:

 Bash command                       │   Claude has written up a plan and is ready
                                    │   to execute. Would you like to proceed?
   touch /tmp/marker                │
   Create marker file in /tmp       │   ❯ 1. Yes, and use auto mode
                                    │     2. Yes, manually approve edits
 Do you want to proceed?            │     3. No, refine with Ultraplan …
 ❯ 1. Yes                           │     4. Tell Claude what to change
   2. Yes, and always allow …       │
   3. No                            │

And it's worse than invisible: while a dialog is up the pane shows neither the "esc to interrupt" hint nor the mode footer, so paneBusy reads False and every surface labelled the session idle. A session actually blocked on a human read as finished — so you'd only discover it by opening the terminal, which is also the only place you could answer.

The change

  • parse_pane_prompt reads the dialog off the same capture as the mode marker → {prompt, options:[{number,label,selected}], detail}, reported as session.panePrompt.
    • Nothing keys on the wording (it differs per dialog, and will drift). A run of lines is a dialog only with all of: options numbered 1..N (N≥2) in order, exactly one carrying the cursor, a ? line directly above, and no mode footer below. That last one is the discriminator — the footer rides the composer, which a dialog replaces, so its absence separates a live dialog from transcript text shaped like one.
    • detail is the fenced block above the question — the command plus its description, or the plan body. Blanks never close it, a rule does: a permission dialog's block sits directly above the question while a plan's sits one rule further up, and one walk has to find both.
  • Rendered in the chat's existing pending-question box (a session is never blocked on both — one blocks in the ask.py hook, the other in the TUI) with the context and numbered picks, the TUI's own cursor position marked.
  • Answerable: POST .../sessions/<id>/pane-prompt {optionNumber}answerPanePrompt → the agent types that digit. The agent re-reads the pane first and drops the answer unless the number is on screen right now — the click was made against a beat-old heartbeat, and a bare digit typed into a live composer silently prepends itself to the operator's next message.
  • Both liveStates (dashboard + sessions) check it ahead of the busy read, so the card reads "waiting for your answer" rather than idle; Stop is hidden while a dialog waits, same reasoning as XERK-21.

Verification

  • Agent 803 + turma 634 suites green. The parser and answer-command tests are driven by verbatim tmux capture-pane output captured from two live dialogs (a real Bash permission request and a real plan approval on Claude Code 2.1.220), not hand-written strings — the wordings, glyphs and blank-line placement are the contract, and inventing them is how a parser passes its tests and fails a pane. Negative controls cover an idle composer showing a numbered list with a "?" above it, missing cursor, numbering not starting at 1, and a single option.
  • End-to-end in Chromium against a booted hub: the dialog renders with its context and picks, clicking option 2 queues answerPanePrompt {optionNumber: 2} on the agent's command queue, the sidebar header reports "1 running · 1 waiting on you" and the card reads "waiting for your answer" (it read plain idle before this change). Checked light, dark, and 390px.

Notes

  • Android decodes the new field away today (safe degrade — such a session still reads idle there); gap recorded in android/PARITY.md.
  • CLAUDE.md is over its own 150k budget (161k on main before this PR, ~163k after). Not introduced here and not something I wanted to fold into this change silently — worth a consolidation pass of its own.

…there

Claude Code blocks a turn on two dialogs the chat could not see: a tool
permission request ("Bash command ... Do you want to proceed?") and a plan
approval. Neither has a hook to intercept it and neither writes anything to
the transcript, so the chat showed nothing at all.

Worse, while a dialog is up the pane shows NEITHER the "esc to interrupt"
hint NOR the mode footer, so paneBusy reads False and every surface labelled
the session idle. A session actually blocked on a human read as finished,
and the only way to see it - or answer - was the raw terminal. That was the
last routine reason to leave the chat page.

- parse_pane_prompt reads the dialog off the same capture as the mode
  marker: {prompt, options:[{number,label,selected}], detail}. Nothing keys
  on the wording (it differs per dialog); a line run qualifies only with
  options numbered 1..N, exactly one ❯ cursor, a "?" line above, and no mode
  footer below - the footer rides the composer a dialog replaces, so its
  absence is what separates a live dialog from transcript text shaped like
  one. detail carries the command + its description, or the plan body.
- Reported as session.panePrompt, rendered in the chat's pending-question
  box with numbered picks, and answered via
  POST .../sessions/<id>/pane-prompt {optionNumber} -> answerPanePrompt,
  which types the digit. The agent RE-READS the pane first and drops the
  answer unless that number is on screen: the click was made against a
  beat-old heartbeat, and a stray digit typed into a live composer prepends
  itself to the operator's next message.
- Both liveStates check it ahead of the busy read, so the card reads
  "waiting for your answer" instead of idle, and Stop is hidden while a
  dialog waits (same reasoning as XERK-21 for questions).

Verified: agent 803 + turma 634 green, with the parser and answer tests
driven by verbatim capture-pane output from two live dialogs rather than
hand-written strings. Exercised in Chromium end to end - the dialog renders
with its context and picks, clicking option 2 queues
answerPanePrompt{optionNumber:2}, the sidebar reports "1 waiting on you",
checked light/dark/390px. Android gap recorded in PARITY.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant