Fix stale UI prompt after a permission/question is answered in the CLI#44
Open
nithiink wants to merge 2 commits into
Open
Fix stale UI prompt after a permission/question is answered in the CLI#44nithiink wants to merge 2 commits into
nithiink wants to merge 2 commits into
Conversation
AskUserQuestion menus and the ExitPlanMode dialog render in the live TUI, so the user can answer them with the keyboard. When they do, no voice answer() fires, so the runner's `s.pending` was never cleared — list() and poll_status kept reporting a prompt the user had already dealt with, and the UI's prompt card stuck around. Backend: the per-session event tail now polls the pane (throttled) while a CLI-answerable prompt is pending. Once that prompt's menu/dialog has been seen on screen and then leaves it, the prompt was answered in the CLI, so `s.pending` is retired (two-strike confirm guards against a capture caught mid-redraw; identity-keying to the Prompt avoids clearing one whose menu hasn't rendered yet). Risky-tool permission prompts park the hook with no on-screen menu and are deliberately never reconciled this way. Frontend: the 2s session-list poll now reconciles the prompt card against the backend — if the card's session no longer reports a pending prompt, the card is dismissed. A timestamp guard prevents a list() response already in flight before the card went up from clearing a freshly-raised prompt. Adds backend regression tests for _reconcile_pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the conflict in backend/tmux_runner.py's _TmuxSession.__init__, where both branches added new instance fields at the same spot: - main #32 (one-decision-per-prompt): prompt_seq / answer_claimed - this branch (CLI-answer reconcile): _pending_seen / _pending_gone_strikes Both field sets are kept — the mechanisms are orthogonal. #32 guards against duplicate VOICE answers racing; this branch retires a prompt the user answered with the KEYBOARD in the live TUI. They cooperate cleanly: _reconcile_pending skips while _turn_lock is held (a voice answer is driving the menu), and when it clears s.pending an in-flight voice answer then fails #32's `s.pending is None or s.prompt_seq != seq` guard rather than writing a stale decision. A stale answer_claimed self-heals on the next park (prompt_seq bumps), so no reset is needed in the reconcile path. Verified on the merged tree: 46 backend tests pass, frontend tsc clean, and a live tmux+claude e2e run confirms a CLI-answered AskUserQuestion prompt is retired (list() goes clean) with #32's prompt_seq/answer_claimed and #46's ALLOWED_PROJECT_ROOTS sandbox both active. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a Claude session is being co-driven by voice and keyboard, the user can answer a prompt directly in the CLI (the live TUI). For prompts that render an on-screen menu/dialog — AskUserQuestion menus and the ExitPlanMode approval dialog — the PreToolUse hook emits
allowand lets the TUI draw the menu, which the user can answer with the keyboard.When they do, no voice
answer()fires, so the runner'ss.pendingwas never cleared.list()andpoll_statuskept reporting a prompt the user had already dealt with, and the browser/phone UI kept showing a stale prompt card.(Risky-tool permission prompts are not affected — their hook parks with no on-screen menu, so they can only be resolved by voice/mode/interrupt.)
Fix
Backend (
tmux_runner.py) — the per-session event tail now polls the pane (throttled to ~0.4s, since a capture is a subprocess) while a CLI-answerable prompt is pending. Once that prompt's menu/dialog has been seen on screen and then leaves it, the prompt was answered in the CLI, sos.pendingis retired andstatusreturns torunning(a laterturn_completeflips it tocompleted).Guards:
_pending_seenis the veryPromptobject, so a freshly-raised prompt whose menu hasn't rendered yet is never cleared prematurely.answer()holds_turn_lock(it is itself driving the menu).choiceprompts andExitPlanModeare reconciled; parked-hook permissions never are.Frontend (
VoiceAgent.tsx) — the always-on 2s session-list poll now reconciles the prompt card against the backend: if the card's session no longer reports a pending prompt (and isn't in aneeds_*status), the card is dismissed. AnowMs()timestamp guard prevents alist()response that was already in flight before the card went up from clearing a freshly-raised prompt.Tests
Adds
backend/tests/test_cli_answer_reconcile.pycovering: clear after menu leaves screen, no clear before the menu renders, single-poll flicker is ignored, risky permissions are never reconciled, the ExitPlanMode dialog case, and skip-while-locked.tsc --noEmitpasses clean on the frontend.🤖 Generated with Claude Code