Skip to content
This repository was archived by the owner on Sep 4, 2026. It is now read-only.
This repository was archived by the owner on Sep 4, 2026. It is now read-only.

AskUserQuestion is unanswerable: generic Allow/Deny prompt, then "the user did not answer" #364

Description

@ElbertePlinio

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_approvemanager.rs:1429 approvesrc-tauri/src/agent_chat_commands.rs:281src/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

  • An AskUserQuestion renders its questions and options; the generic Allow/Deny prompt never appears for it.
  • Answering sends answers keyed by exact question text; the agent's tool result reads "Your questions have been answered", never "The user did not answer the questions."
  • Multi-select produces a ", "-joined string; notes and free text arrive as annotations / response.
  • "Allow for session" can never auto-answer a later question.
  • Declining lets the model rephrase instead of failing the turn.

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.tsquestions 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-ideAgent IDE last-milebugSomething isn't workinguxUX / interaction polish

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions