claude: degrade AskUserQuestion to a text question when viewed in chat mode#353
Merged
Conversation
Foundation for the AskUserQuestion chat-gate: clients report which surface (chat vs terminal) they're viewing a session through, and the daemon can be asked whether any chat viewer is active. Phase-0 verified that a PreToolUse deny cleanly suppresses Claude's native picker in a live PTY (no widget, tidy inline error, model re-asks as text).
- daemon: track (conn_id -> session, view) per client connection; clear on disconnect; answer session.chat_viewer_active. Wire session.set_view + session.chat_viewer_active into dispatch. - client: set_view / chat_viewer_active methods. - cli: hidden 'agent ask-gate' PreToolUse hook body — denies (degrade to text) only when a chat viewer is confirmed active; fails open otherwise. Verified live: gate allows with no viewer and fails open on unknown session.
Both interactive and headless arg paths now pass --settings pointing the AskUserQuestion matcher at 'agent ask-gate'. Verified --settings merges with the user's existing hooks (doesn't clobber). Opt out via AGENTD_CLAUDE_ASKGATE=0.
Activates the AskUserQuestion chat-gate: both clients now tell the daemon which surface they're showing the focused session through. - TUI: debounced report_view() once per loop iteration. - webui: reportView() on selectSession + view-mode toggle (terminal only when actually viewing a live PTY). Plus the test_app constructor field. Full chain verified end-to-end earlier: a chat viewer makes the gate deny, which makes Claude re-ask AskUserQuestion as plain text; disconnect clears it.
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 runs interactively,
AskUserQuestionrenders as a native TUI picker inside the PTY. agentd's chat view shows the structured transcript, not the live PTY — so a user watching in chat mode sees the question arrive as an inert tool call and has no way to answer it without switching to the terminal view. (Headless mode is worse: Claude auto-dismissesAskUserQuestionentirely.)We explored reconstructing the picker natively in chat, or driving Claude's widget via keystrokes — both are brittle: we can't read the widget's cursor state, and Claude can change its widget/keymap at any time. Driving an opaque TUI we don't own risks committing the wrong answer silently.
Approach — let Claude ask in plain text, but only when it matters
A
PreToolUsehook onAskUserQuestion, injected by the adapter, denies the tool with a reason instructing Claude to ask the question as a plain-text message with the options inline. Claude complies, and that text question is visible in chat and answerable via the normal send-message path. Conditional: the hook only degrades when a chat viewer is active for that session — terminal viewers keep Claude's native picker untouched.Key property: we suppress the native picker (deny before it renders) rather than reconstruct or drive it — so there's nothing brittle to break, and it's immune to Claude changing its widgets.
Changes
ClientView{Chat,Terminal};session.set_view/session.chat_viewer_active.(conn → session, view)per client connection; clear on disconnect; answerchat_viewer_active.set_view+chat_viewer_active; hiddenagent ask-gatehook body. Fails open (any error/timeout/ambiguity → allow) so it never strands the model.--settingsregistering the gate hook (interactive and headless).--settingsmerges with the user's settings. Opt out withAGENTD_CLAUDE_ASKGATE=0.set_view(terminal only when actually viewing a live PTY).Verification (each link proven against the real
claudebinary)AskUserQuestionmid-turn suppresses the picker cleanly — no widget, tidy inline error, model re-asks as text, turn completes.1) … 2) …).--settingsmerge: a project hook and the injected hook both fire — no clobbering.ask-gatedeny; disconnect makes it allow (per-connection cleanup); unknown/no viewer → allow.Notes
is_error:trueon the synthesized tool_result; the model handles it correctly (treats the reason as guidance and re-asks).AskUserQuestionanswerable-as-text when a chat viewer is present, instead of auto-dismissing.Binaries
Code lives in
agentd(daemon),agent(CLI),agentd-adapter-claude.