Goal
Render AskUserQuestion as an answerable question card and send the user's real answers back to the agent, instead of a meaningless Allow/Deny prompt followed by "the user did not answer".
Why
The agent asked three questions. PickForge showed a generic approval prompt labelled only TOOL AskUserQuestion — no questions, no options. Pressing Allow made the agent report "No answer — I'll proceed with the recommended defaults" and continue on guesses.
That is worse than a hard failure: the user believes they answered, the agent believes they declined to, and the turn proceeds on invented defaults. Any AskUserQuestion in a Claude chat is currently unanswerable.
Verified against SDK 0.3.198 / bundled CLI 2.1.198 (package.json:25).
Root cause
The permission prompt is not an SDK bug — it is the hand-off. AskUserQuestion declares requiresUserInteraction() { return true }, and that check runs before the bypass branch in the SDK's permission evaluator. So the tool routes through canUseTool even under Bypass permissions, by design, and its checkPermissions() returns {behavior: "ask", updatedInput: {questions}} — the SDK is handing the host the questions to render. The SDK's own TUI answers it by returning {behavior: "allow", updatedInput: {...input, answers, annotations}}.
PickForge treats it as an ordinary allow/deny, and loses the questions at three separate layers:
1. The bridge allows without answering. scripts/claude-bridge.ts:280-291:
if (decision === "accept" || decision === "acceptForSession") {
return { behavior: "allow", updatedInput: input };
}
input here is {questions, metadata?} — the SDK strips answers before the callback. With no answers key, the tool's own result text is literally "The user did not answer the questions." That is the string the user saw rendered as "No answer".
2. The store drops the questions. parseApprovalDetail (src/stores/agentChat.ts:380-406) extracts only command / cwd / reason / toolName from the approval detail. The full input — including questions — arrives intact on AgentApproval.detail (claude_bridge.rs:734-744 preserves it) and is never read.
3. The card has nothing to show. ApprovalPrompt.tsx:13-15:
function headline(approval: AgentApproval): string {
return approval.parsed?.command ?? approval.parsed?.toolName ?? approval.detail;
}
For any non-Bash tool that resolves to the tool's bare name — hence TOOL AskUserQuestion and nothing else. There is no per-tool renderer anywhere in the approval path (claude_bridge.rs:882-888 and ApprovalPrompt.tsx:7-11 are the only tool/kind switches, neither covers this).
The contract to satisfy
Return from canUseTool:
{ behavior: "allow", updatedInput: { ...input, answers, annotations?, response? } }
answers: Record<questionText, string> — keyed by the exact question string, not header.
- Multi-select answers are a single
", "-joined string ("A, B"), not an array.
annotations: Record<questionText, { preview?, notes? }> for the selected option's preview and the user's free-text notes.
- Free text goes in
updatedInput.response, not in answers.
- An empty
answers object still yields the "did not answer" fallback — omit empty keys entirely.
Scope
A1. Bridge — answer, don't merely allow. scripts/claude-bridge.ts
- Extend the approve op with an optional structured payload
{answers, annotations?, response?}; the existing four decisions stay untouched for every other tool.
permissionResultForDecision (:280) builds the updatedInput above when a payload is present.
- Exempt
AskUserQuestion from gate.alwaysAllow (:311-312). approvalScopeKey (:72-84) collapses every question to AskUserQuestion\0"", so one "Allow for session" would silently auto-answer every future question with a stale reply. This is a latent data-corruption path, not a nicety.
- Map
decline to the SDK's clarify shape so the model can rephrase rather than hard-fail.
A2. Core — carry the kind and the payload.
event.rs:171 — add ApprovalKind::Question.
claude_bridge.rs:882 approval_kind — "AskUserQuestion" → Question.
- Thread the optional answer payload through
claude_bridge.rs:332 chat_approve → manager.rs:1429 approve → src-tauri/src/agent_chat_commands.rs:281 → src/lib/agentChat.ts:351. No new event field is needed inbound: detail already carries the full input.
A3. Store + card.
parseApprovalDetail (agentChat.ts:380) surfaces typed questions; approveAgentRequest (:2038) forwards the payload.
- New
src/components/chat/QuestionPrompt.tsx, dispatched from ApprovalPrompt.tsx on kind === "question". Full parity with the CLI: option label + description, multiSelect where asked, free-text "Other", per-question notes → annotations. Submit disabled until every question is answered; secondary action maps to clarify.
- Design: run
design-director. Two standing rules for this app — status reads as bracket-cornered mono text, never a filled chip; telemetry anchors to an edge that already exists rather than adding chrome.
Acceptance criteria
Validation
tests/unit/claudeBridge.test.ts — regression first (allow-without-answers is the current broken shape), then the answered path, multi-select joining, and the alwaysAllow exemption.
- Rust:
approval_kind("AskUserQuestion") == Question; the payload reaches the child's stdin (extend the fake-bridge stdin-log assertion at claude_bridge.rs:1347).
tests/unit/agentChat.test.ts — questions parsed from detail; payload reaches invoke("agent_chat_approve", …).
- Component test for
QuestionPrompt (single, multi, notes, Other) + a VRT fixture — there is no approvalRequest in tauriMock.ts today.
- Live E2E, new tier. No test in this repo reaches the real CLI: bridge tests mock the SDK and put a zero-byte
claude on PATH (claudeBridge.test.ts:94), Rust tests spawn #!/bin/sh fakes, VRT runs against tauriMock.ts, bun run e2e mocks invoke. Add an opt-in tests/e2e/claude-live.ts, env-gated in the style of the existing device tiers and skipped in CI, that runs the real bridge against the real claude CLI on Haiku 4.5, forces an AskUserQuestion, answers it through the approve op, and asserts the resulting tool result.
- Manual: run the app from the worktree with a distinct dev port and version suffix per
AGENTS.md, answer a three-question prompt, confirm the agent proceeds on the given answers.
Flag
No flag. Self-contained bug fix on an existing surface, shipping in the next release — the release tag is the switch (workspace AGENTS.md, "Feature flags").
Notes
Goal
Render
AskUserQuestionas an answerable question card and send the user's real answers back to the agent, instead of a meaningless Allow/Deny prompt followed by "the user did not answer".Why
The agent asked three questions. PickForge showed a generic approval prompt labelled only
TOOL AskUserQuestion— no questions, no options. Pressing Allow made the agent report "No answer — I'll proceed with the recommended defaults" and continue on guesses.That is worse than a hard failure: the user believes they answered, the agent believes they declined to, and the turn proceeds on invented defaults. Any
AskUserQuestionin a Claude chat is currently unanswerable.Verified against SDK
0.3.198/ bundled CLI2.1.198(package.json:25).Root cause
The permission prompt is not an SDK bug — it is the hand-off.
AskUserQuestiondeclaresrequiresUserInteraction() { return true }, and that check runs before the bypass branch in the SDK's permission evaluator. So the tool routes throughcanUseTooleven under Bypass permissions, by design, and itscheckPermissions()returns{behavior: "ask", updatedInput: {questions}}— the SDK is handing the host the questions to render. The SDK's own TUI answers it by returning{behavior: "allow", updatedInput: {...input, answers, annotations}}.PickForge treats it as an ordinary allow/deny, and loses the questions at three separate layers:
1. The bridge allows without answering.
scripts/claude-bridge.ts:280-291:inputhere is{questions, metadata?}— the SDK stripsanswersbefore the callback. With noanswerskey, the tool's own result text is literally"The user did not answer the questions."That is the string the user saw rendered as "No answer".2. The store drops the questions.
parseApprovalDetail(src/stores/agentChat.ts:380-406) extracts onlycommand/cwd/reason/toolNamefrom the approvaldetail. The full input — includingquestions— arrives intact onAgentApproval.detail(claude_bridge.rs:734-744preserves it) and is never read.3. The card has nothing to show.
ApprovalPrompt.tsx:13-15:For any non-Bash tool that resolves to the tool's bare name — hence
TOOL AskUserQuestionand nothing else. There is no per-tool renderer anywhere in the approval path (claude_bridge.rs:882-888andApprovalPrompt.tsx:7-11are the only tool/kind switches, neither covers this).The contract to satisfy
Return from
canUseTool:answers:Record<questionText, string>— keyed by the exactquestionstring, notheader.", "-joined string ("A, B"), not an array.annotations:Record<questionText, { preview?, notes? }>for the selected option's preview and the user's free-text notes.updatedInput.response, not inanswers.answersobject still yields the "did not answer" fallback — omit empty keys entirely.Scope
A1. Bridge — answer, don't merely allow.
scripts/claude-bridge.ts{answers, annotations?, response?}; the existing four decisions stay untouched for every other tool.permissionResultForDecision(:280) builds theupdatedInputabove when a payload is present.AskUserQuestionfromgate.alwaysAllow(:311-312).approvalScopeKey(:72-84) collapses every question toAskUserQuestion\0"", so one "Allow for session" would silently auto-answer every future question with a stale reply. This is a latent data-corruption path, not a nicety.declineto the SDK's clarify shape so the model can rephrase rather than hard-fail.A2. Core — carry the kind and the payload.
event.rs:171— addApprovalKind::Question.claude_bridge.rs:882approval_kind—"AskUserQuestion"→Question.claude_bridge.rs:332chat_approve→manager.rs:1429approve→src-tauri/src/agent_chat_commands.rs:281→src/lib/agentChat.ts:351. No new event field is needed inbound:detailalready carries the full input.A3. Store + card.
parseApprovalDetail(agentChat.ts:380) surfaces typedquestions;approveAgentRequest(:2038) forwards the payload.src/components/chat/QuestionPrompt.tsx, dispatched fromApprovalPrompt.tsxonkind === "question". Full parity with the CLI: option label + description,multiSelectwhere asked, free-text "Other", per-question notes →annotations. Submit disabled until every question is answered; secondary action maps to clarify.design-director. Two standing rules for this app — status reads as bracket-cornered mono text, never a filled chip; telemetry anchors to an edge that already exists rather than adding chrome.Acceptance criteria
AskUserQuestionrenders its questions and options; the generic Allow/Deny prompt never appears for it.answerskeyed by exact question text; the agent's tool result reads "Your questions have been answered", never "The user did not answer the questions."", "-joined string; notes and free text arrive asannotations/response.Validation
tests/unit/claudeBridge.test.ts— regression first (allow-without-answers is the current broken shape), then the answered path, multi-select joining, and thealwaysAllowexemption.approval_kind("AskUserQuestion") == Question; the payload reaches the child's stdin (extend the fake-bridge stdin-log assertion atclaude_bridge.rs:1347).tests/unit/agentChat.test.ts—questionsparsed fromdetail; payload reachesinvoke("agent_chat_approve", …).QuestionPrompt(single, multi, notes, Other) + a VRT fixture — there is noapprovalRequestintauriMock.tstoday.claudeonPATH(claudeBridge.test.ts:94), Rust tests spawn#!/bin/shfakes, VRT runs againsttauriMock.ts,bun run e2emocksinvoke. Add an opt-intests/e2e/claude-live.ts, env-gated in the style of the existing device tiers and skipped in CI, that runs the real bridge against the realclaudeCLI on Haiku 4.5, forces anAskUserQuestion, answers it through the approve op, and asserts the resulting tool result.AGENTS.md, answer a three-question prompt, confirm the agent proceeds on the given answers.Flag
No flag. Self-contained bug fix on an existing surface, shipping in the next release — the release tag is the switch (workspace
AGENTS.md, "Feature flags").Notes
ExitPlanModealso declaresrequiresUserInteractionand will hit the same generic prompt. Out of scope; follow-up issue to be filed from this PR.status/detailplumbing.