🤖 feat: add ask_user_question interactive plan-mode tool #1121
+1,163
−35
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.
Adds a new interactive plan-mode tool
ask_user_question(inspired by Claude Code'sAskUserQuestion) and a corresponding inline UI for answering multiple-choice questions.Key behaviors:
tool-error) and the message is sent as a real user message.Validation:
make static-check📋 Implementation Plan
🤖 Plan: Add an
AskUserQuestion-style tool to Mux (Plan Mode UX)Goal
Implement a tool compatible with Claude Code’s
AskUserQuestionsemantics (percc-plan-mode.md) so that, while staying in plan mode, the assistant can ask the user 1–4 multiple-choice questions and receive structured answers.Key UX requirement: the question UI should be non-invasive and optional—the user is encouraged to answer via the form, but they should also be able to just type a normal chat reply without having to explicitly “close/deny” the UI.
Source-of-truth behavior to mirror (from
cc-plan-mode.md)We should match the Claude Code behavior + schema as closely as practical, while using Mux’s snake_case naming convention:
AskUserQuestionask_user_question{ questions: Question[], answers?: Record<string,string> }{ questions: Question[], answers: Record<string,string> }optionsinput)What already exists in Mux (relevant implementation hooks)
src/browser/components/Messages/ToolMessage.tsx, which routes known tools to custom React components; otherwise it falls back toGenericToolCall.src/browser/components/tools/shared/ToolPrimitives.tsx+toolUtils.tsx.src/browser/components/ui/toggle-group.tsx(Radix ToggleGroup; supportstype="single"|"multiple")src/browser/components/ui/checkbox.tsxsrc/browser/components/ui/input.tsxsrc/browser/components/ui/button.tsxsrc/common/utils/tools/toolDefinitions.ts(Zod schema + description); backend tools reference this.src/node/services/tools/*and are registered throughsrc/common/utils/tools/tools.ts(getToolsForModel).dynamic-toolparts; while a tool is running it’sstate: "input-available"and UI status becomes"executing".Recommended approach (single approach; net new product LoC ~500–900)
Summary
Add a new interactive tool
ask_user_question(CC-compatibleAskUserQuestion) that:Backend design
1) Tool definition (shared)
ask_user_questiontoTOOL_DEFINITIONSwith a Zod schema matchingcc-plan-mode.md:AskUserQuestionOptionSchema,AskUserQuestionQuestionSchema) so the result schema + UI types can reuse them.Option:{ label: string; description: string }Question:{ question: string; header: string; options: Option[]; multiSelect: boolean }{ questions: Question[]; answers?: Record<string,string> }+ refine uniqueness{ questions: Question[]; answers: Record<string,string> }TypeScript types: infer from Zod (avoid handwritten interfaces) to prevent drift.
export type AskUserQuestionToolArgs = z.infer<typeof TOOL_DEFINITIONS.ask_user_question.schema>;AskUserQuestionToolResultSchema(Zod) alongside the input schema (reusing the sameQuestion/Optionschemas), then:export type AskUserQuestionToolResult = z.infer<typeof AskUserQuestionToolResultSchema>;AskUserQuestionQuestion/AskUserQuestionOptionviaz.infer<...>for the UI reducer.2) Pending-question manager (new node service)
Create an in-memory manager owned by the backend process (NOT persisted):
AskUserQuestionManagerregisterPending(workspaceId, toolCallId, questions): Promise<Record<string,string>>answer(workspaceId, toolCallId, answers): voidcancel(workspaceId, toolCallId, reason): voidgetLatestPending(workspaceId): { toolCallId, questions } | nullImportant defensive behavior:
3) Tool implementation (
src/node/services/tools/ask_user_question.ts)Implement
createAskUserQuestionTool(config)usingtool()fromai.Execution flow:
workspaceIdfromconfig.workspaceId(assert present).toolCallIdfromToolCallOptions.toolCallId.AskUserQuestionManagerand await answers.{ questions, answers }(matching Claude Code’s output schema).Abort/cancel handling:
tool-errorpart (Mux will record this as a failed tool result).Model-context parity (optional but recommended):
applyToolOutputRedaction(...)to replace the provider-facing output forask_user_questionwith a short string summary like:User has answered your questions: "<Q>"="<A>", ... You can now continue with the user's answers in mind.{ questions, answers }persisted for UI.4) ORPC endpoint to submit answers
Add a workspace-scoped ORPC endpoint:
workspace.answerAskUserQuestion{ workspaceId: string; toolCallId: string; answers: Record<string,string> }Result<void, string>Implementation:
WorkspaceService→AskUserQuestionManager.answer(...).5) “Chat message instead of clicking” behavior (non-invasive requirement)
Requirement (confirmed): if the user just types a normal chat reply while
ask_user_questionis pending, we should not treat it as tool input. Instead we should:Implementation sketch:
AskUserQuestionManager.getLatestPending(workspaceId)(orgetPendingByToolCallId) that returns the active pending prompt.WorkspaceService.sendMessage()behavior:aiService.isStreaming(workspaceId)and a pendingask_user_questionexists:AskUserQuestionManager.cancel(workspaceId, toolCallId, "User responded in chat; questions canceled")session.queueMessage(message, options))Ok()Effect:
tool-error→ Mux records a failed tool result.Frontend design
1) Add a custom tool renderer
Add
AskUserQuestionToolCall.tsxundersrc/browser/components/tools/and route it inToolMessage.tsxsimilarly toProposePlanToolCall.Props needed:
args,result,status,workspaceId2) UI behavior (inline form)
Render as an inline tool card, expanded by default while awaiting answers:
ask_user_question+ statusheader, with a checkmark for answered.ToggleGroup type="single"Checkboxlist orToggleGroup type="multiple"Inputfor custom textSubmit answersbutton → callsapi.workspace.answerAskUserQuestion({ workspaceId, toolCallId, answers })State management:
currentQuestionIndex,answers,questionStates,isInTextInput).Accessibility/keyboard (follow-up, but planned):
3) Display after completion
When
resultis present:4) Non-invasive encouragement
While the tool is pending:
(We can later refine this into a better “waiting for input” streaming banner, but it’s not required for v1.)
Testing / validation (net new test LoC ~250–450)
AskUserQuestionManager:ask_user_questiontool callAlternatives considered
A) Non-blocking “suggestion card” tool (net new product LoC ~300–600)
Have
ask_user_questionreturn immediately and not pause the stream; user answers later and the answer is posted as a normal user message.Downside: this diverges from Claude Code semantics and won’t behave like the tool LLMs were trained on (tool answers won’t be available in the same turn).
Decisions / requirements confirmed
ask_user_questionis pending:ask_user_question(snake_case).Availability / gating
ask_user_questiononly whenSendMessageOptions.mode === "plan"(so it is available by default in plan mode, and not callable in exec mode).AIService.streamMessage()passesmodeinto the earlygetToolsForModel(...)call used for the mode-transition “Available tools: …” sentinel. Otherwise plan mode would omitask_user_questionfrom the advertised tool list even though it’s registered.ask_user_questiontool parts (no toggle).Net new product code estimate (recommended approach): ~500–900 LoC
Generated with
mux