chenbhao commited on
Commit
a8a1ac1
·
1 Parent(s): 6dc252c

feat(tool): decouple AskUserQuestion into standalone question channel

Browse files

Add a dedicated question service and overlay instead of routing
AskUserQuestion through the permission system. Wire tab/arrow keybindings
in the standalone prompt and clarify the tool prompt with an explicit
parameter-structure example to reduce first-try schema errors.

src/components/question/QuestionPrompt.tsx ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import React, { useCallback, useState } from "react";
3
+ import type { Question } from "../../tools/AskUserQuestionTool/AskUserQuestionTool.js";
4
+ import { questionService, type QuestionRequest } from "../../services/question/questionService.js";
5
+ import { useMultipleChoiceState } from "../permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.js";
6
+ import { QuestionView } from "../permissions/AskUserQuestionPermissionRequest/QuestionView.js";
7
+ import { SubmitQuestionsView } from "../permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.js";
8
+ import { useRegisterOverlay } from "../../context/overlayContext.js";
9
+ import { useKeybindings } from "../../keybindings/useKeybinding.js";
10
+ import type { PermissionDecision } from "../../utils/permissions/PermissionResult.js";
11
+
12
+ const NO_PERMISSION: PermissionDecision = { behavior: "allow" } as PermissionDecision;
13
+
14
+ type Props = {
15
+ request: QuestionRequest;
16
+ onResolved: () => void;
17
+ };
18
+
19
+ /**
20
+ * Standalone question overlay (decoupled from the permission system).
21
+ * Renders the same choice UI as AskUserQuestion but resolves the
22
+ * questionService pending request on reply/reject.
23
+ */
24
+ export function QuestionPrompt(props: Props) {
25
+ const $ = _c(40);
26
+ const { request, onResolved } = props;
27
+ const questions = request.questions as unknown as Question[];
28
+ const [answers, setAnswers] = useState<Record<string, string>>({});
29
+ const state = useMultipleChoiceState();
30
+ const {
31
+ currentQuestionIndex,
32
+ questionStates,
33
+ isInTextInput,
34
+ nextQuestion,
35
+ prevQuestion,
36
+ updateQuestionState,
37
+ setAnswer,
38
+ setTextInputMode,
39
+ } = state;
40
+ const currentQuestion =
41
+ currentQuestionIndex < questions.length ? questions[currentQuestionIndex] : null;
42
+ const isInSubmitView = currentQuestionIndex === questions.length;
43
+ const allQuestionsAnswered = questions.every(q => q?.question && !!answers[q.question]);
44
+ const hideSubmitTab = questions.length === 1 && !questions[0]?.multiSelect;
45
+ const maxIndex = hideSubmitTab ? questions.length - 1 : questions.length;
46
+ useRegisterOverlay("question", true);
47
+ let t0;
48
+ if ($[0] !== answers || $[1] !== questions) {
49
+ t0 = questions.every(q_0 => q_0?.question && !!answers[q_0.question]) ?? false;
50
+ $[0] = answers;
51
+ $[1] = questions;
52
+ $[2] = t0;
53
+ } else {
54
+ t0 = $[2];
55
+ }
56
+ let t1;
57
+ if ($[3] !== answers) {
58
+ t1 = (q_1: string, a: string | string[], textInput?: string, shouldAdvance?: boolean) => {
59
+ const isMulti = Array.isArray(a);
60
+ const value = isMulti ? (a as string[]).join(", ") : (a as string);
61
+ const next = { ...answers, [q_1]: value };
62
+ setAnswers(next);
63
+ setAnswer(q_1, value, shouldAdvance);
64
+ if (!isMulti && shouldAdvance !== false && questions.length === 1) {
65
+ void reply([value]);
66
+ }
67
+ };
68
+ $[3] = answers;
69
+ $[4] = t1;
70
+ } else {
71
+ t1 = $[4];
72
+ }
73
+ const handleAnswer = t1;
74
+ let t2;
75
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
76
+ t2 = (q_2: string) => {
77
+ const next = { ...answers };
78
+ delete next[q_2];
79
+ setAnswers(next);
80
+ };
81
+ $[5] = t2;
82
+ } else {
83
+ t2 = $[5];
84
+ }
85
+ const handleCancel = t2;
86
+ let t3;
87
+ if ($[6] !== answers || $[7] !== questions) {
88
+ t3 = (final: "submit" | "cancel") => {
89
+ if (final === "cancel") {
90
+ handleCancelReply();
91
+ return;
92
+ }
93
+ const result = questions.map(q_3 => {
94
+ const a = answers[q_3.question];
95
+ return a ? (q_3.multiSelect ? a.split(", ") : [a]) : [];
96
+ });
97
+ void reply(result);
98
+ };
99
+ $[6] = answers;
100
+ $[7] = questions;
101
+ $[8] = t3;
102
+ } else {
103
+ t3 = $[8];
104
+ }
105
+ const handleFinal = t3;
106
+ let t4;
107
+ if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
108
+ t4 = () => {
109
+ questionService.reject(request.id);
110
+ onResolved();
111
+ };
112
+ $[9] = t4;
113
+ } else {
114
+ t4 = $[9];
115
+ }
116
+ const handleCancelReply = t4;
117
+ let t5;
118
+ if ($[10] !== answers || $[11] !== questions) {
119
+ t5 = async (result: string[][]) => {
120
+ questionService.reply({ requestID: request.id, answers: result });
121
+ onResolved();
122
+ };
123
+ $[10] = answers;
124
+ $[11] = questions;
125
+ $[12] = t5;
126
+ } else {
127
+ t5 = $[12];
128
+ }
129
+ const reply = t5;
130
+ let t6;
131
+ if ($[13] !== currentQuestionIndex || $[14] !== maxIndex || $[15] !== nextQuestion) {
132
+ t6 = () => {
133
+ if (currentQuestionIndex < maxIndex) nextQuestion();
134
+ };
135
+ $[13] = currentQuestionIndex;
136
+ $[14] = maxIndex;
137
+ $[15] = nextQuestion;
138
+ $[16] = t6;
139
+ } else {
140
+ t6 = $[16];
141
+ }
142
+ const handleTabNext = t6;
143
+ let t7;
144
+ if ($[17] !== currentQuestionIndex || $[18] !== prevQuestion) {
145
+ t7 = () => {
146
+ if (currentQuestionIndex > 0) prevQuestion();
147
+ };
148
+ $[17] = currentQuestionIndex;
149
+ $[18] = prevQuestion;
150
+ $[19] = t7;
151
+ } else {
152
+ t7 = $[19];
153
+ }
154
+ const handleTabPrev = t7;
155
+ useKeybindings(
156
+ { "tabs:previous": handleTabPrev, "tabs:next": handleTabNext },
157
+ { context: "Tabs", isActive: !(isInTextInput && !isInSubmitView) },
158
+ );
159
+ if (currentQuestion) {
160
+ let t8;
161
+ if (
162
+ $[20] !== answers ||
163
+ $[21] !== currentQuestion ||
164
+ $[22] !== currentQuestionIndex ||
165
+ $[23] !== handleAnswer ||
166
+ $[24] !== handleCancel ||
167
+ $[25] !== handleTabNext ||
168
+ $[26] !== handleTabPrev ||
169
+ $[27] !== hideSubmitTab ||
170
+ $[28] !== nextQuestion ||
171
+ $[29] !== questions ||
172
+ $[30] !== questionStates ||
173
+ $[31] !== setTextInputMode ||
174
+ $[32] !== updateQuestionState
175
+ ) {
176
+ t8 = (
177
+ <QuestionView
178
+ question={currentQuestion}
179
+ questions={questions}
180
+ currentQuestionIndex={currentQuestionIndex}
181
+ answers={answers}
182
+ questionStates={questionStates}
183
+ hideSubmitTab={hideSubmitTab}
184
+ onUpdateQuestionState={updateQuestionState}
185
+ onAnswer={handleAnswer}
186
+ onTextInputFocus={setTextInputMode}
187
+ onCancel={handleCancelReply}
188
+ onSubmit={nextQuestion}
189
+ onTabPrev={handleTabPrev}
190
+ onTabNext={handleTabNext}
191
+ onRespondToClaude={handleCancelReply}
192
+ onFinishPlanInterview={handleCancelReply}
193
+ />
194
+ );
195
+ $[20] = answers;
196
+ $[21] = currentQuestion;
197
+ $[22] = currentQuestionIndex;
198
+ $[23] = handleAnswer;
199
+ $[24] = handleCancel;
200
+ $[25] = handleTabNext;
201
+ $[26] = handleTabPrev;
202
+ $[27] = hideSubmitTab;
203
+ $[28] = nextQuestion;
204
+ $[29] = questions;
205
+ $[30] = questionStates;
206
+ $[31] = setTextInputMode;
207
+ $[32] = updateQuestionState;
208
+ $[33] = t8;
209
+ } else {
210
+ t8 = $[33];
211
+ }
212
+ return t8;
213
+ }
214
+ if (isInSubmitView) {
215
+ let t9;
216
+ if (
217
+ $[34] !== allQuestionsAnswered ||
218
+ $[35] !== answers ||
219
+ $[36] !== currentQuestionIndex ||
220
+ $[37] !== questions ||
221
+ $[38] !== handleFinal
222
+ ) {
223
+ t9 = (
224
+ <SubmitQuestionsView
225
+ questions={questions}
226
+ currentQuestionIndex={currentQuestionIndex}
227
+ answers={answers}
228
+ allQuestionsAnswered={allQuestionsAnswered}
229
+ permissionResult={NO_PERMISSION}
230
+ onFinalResponse={handleFinal}
231
+ />
232
+ );
233
+ $[34] = allQuestionsAnswered;
234
+ $[35] = answers;
235
+ $[36] = currentQuestionIndex;
236
+ $[37] = questions;
237
+ $[38] = handleFinal;
238
+ $[39] = t9;
239
+ } else {
240
+ t9 = $[39];
241
+ }
242
+ return t9;
243
+ }
244
+ return null;
245
+ }
src/screens/REPL.tsx CHANGED
@@ -1556,6 +1556,19 @@ export function REPL({
1556
  reject: (error: Error) => void
1557
  }>
1558
  >([])
 
 
 
 
 
 
 
 
 
 
 
 
 
1559
 
1560
  // Track bridge cleanup functions for sandbox permission requests so the
1561
  // local dialog handler can cancel the remote prompt when the local user
@@ -2672,6 +2685,7 @@ export function REPL({
2672
  | 'sandbox-permission'
2673
  | 'tool-permission'
2674
  | 'prompt'
 
2675
  | 'worker-sandbox-permission'
2676
  | 'elicitation'
2677
  | 'cost'
@@ -2706,6 +2720,7 @@ export function REPL({
2706
  if (allowDialogsWithAnimation && toolUseConfirmQueue[0])
2707
  return 'tool-permission'
2708
  if (allowDialogsWithAnimation && promptQueue[0]) return 'prompt'
 
2709
  // Worker sandbox permission prompts (network access) from swarm workers
2710
  if (allowDialogsWithAnimation && workerSandboxPermissions.queue[0])
2711
  return 'worker-sandbox-permission'
@@ -2789,7 +2804,8 @@ export function REPL({
2789
  useEffect(() => {
2790
  if (!isLoading) return
2791
 
2792
- const isPaused = focusedInputDialog === 'tool-permission'
 
2793
  const now = Date.now()
2794
 
2795
  if (isPaused && pauseStartTimeRef.current === null) {
@@ -2813,8 +2829,12 @@ export function REPL({
2813
  // no 1-frame flash of the wrong scroll position.
2814
  const prevDialogRef = useRef(focusedInputDialog)
2815
  useLayoutEffect(() => {
2816
- const was = prevDialogRef.current === 'tool-permission'
2817
- const now = focusedInputDialog === 'tool-permission'
 
 
 
 
2818
  if (was !== now) repinScroll()
2819
  prevDialogRef.current = focusedInputDialog
2820
  }, [focusedInputDialog, repinScroll])
@@ -6062,6 +6082,19 @@ export function REPL({
6062
  />
6063
  ) : null
6064
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6065
  // Narrow terminals: companion collapses to a one-liner that REPL stacks
6066
  // on its own row (above input in fullscreen, below in scrollback) instead
6067
  // of row-beside. Wide terminals keep the row layout with sprite on the right.
@@ -6150,7 +6183,7 @@ export function REPL({
6150
  >
6151
  <FullscreenLayout
6152
  scrollRef={scrollRef}
6153
- overlay={toolPermissionOverlay}
6154
  bottomFloat={
6155
  feature('BUDDY') && companionVisible && !companionNarrow ? (
6156
  <CompanionFloatingBubble />
 
1556
  reject: (error: Error) => void
1557
  }>
1558
  >([])
1559
+ const [questionRequest, setQuestionRequest] =
1560
+ useState<import('../services/question/questionService.js').QuestionRequest | null>(null)
1561
+ useEffect(() => {
1562
+ const { questionService } = require('../services/question/questionService.js') as typeof import('../services/question/questionService.js')
1563
+ const onAsked = (request: import('../services/question/questionService.js').QuestionRequest) => {
1564
+ setQuestionRequest(request)
1565
+ }
1566
+ questionService.events.on('asked', onAsked)
1567
+ return () => {
1568
+ questionService.events.off('asked', onAsked)
1569
+ }
1570
+ }, [])
1571
+
1572
 
1573
  // Track bridge cleanup functions for sandbox permission requests so the
1574
  // local dialog handler can cancel the remote prompt when the local user
 
2685
  | 'sandbox-permission'
2686
  | 'tool-permission'
2687
  | 'prompt'
2688
+ | 'question'
2689
  | 'worker-sandbox-permission'
2690
  | 'elicitation'
2691
  | 'cost'
 
2720
  if (allowDialogsWithAnimation && toolUseConfirmQueue[0])
2721
  return 'tool-permission'
2722
  if (allowDialogsWithAnimation && promptQueue[0]) return 'prompt'
2723
+ if (allowDialogsWithAnimation && questionRequest) return 'question'
2724
  // Worker sandbox permission prompts (network access) from swarm workers
2725
  if (allowDialogsWithAnimation && workerSandboxPermissions.queue[0])
2726
  return 'worker-sandbox-permission'
 
2804
  useEffect(() => {
2805
  if (!isLoading) return
2806
 
2807
+ const isPaused =
2808
+ focusedInputDialog === 'tool-permission' || focusedInputDialog === 'question'
2809
  const now = Date.now()
2810
 
2811
  if (isPaused && pauseStartTimeRef.current === null) {
 
2829
  // no 1-frame flash of the wrong scroll position.
2830
  const prevDialogRef = useRef(focusedInputDialog)
2831
  useLayoutEffect(() => {
2832
+ const was =
2833
+ prevDialogRef.current === 'tool-permission' ||
2834
+ prevDialogRef.current === 'question'
2835
+ const now =
2836
+ focusedInputDialog === 'tool-permission' ||
2837
+ focusedInputDialog === 'question'
2838
  if (was !== now) repinScroll()
2839
  prevDialogRef.current = focusedInputDialog
2840
  }, [focusedInputDialog, repinScroll])
 
6082
  />
6083
  ) : null
6084
 
6085
+ // Standalone question overlay (decoupled from the permission system).
6086
+ // Reuses the AskUserQuestion choice UI but resolves questionService.
6087
+ const QuestionPromptBox = require('../components/question/QuestionPrompt.js').QuestionPrompt
6088
+
6089
+ const questionOverlay =
6090
+ focusedInputDialog === 'question' && questionRequest ? (
6091
+ <QuestionPromptBox
6092
+ key={questionRequest.id}
6093
+ request={questionRequest}
6094
+ onResolved={() => setQuestionRequest(null)}
6095
+ />
6096
+ ) : null
6097
+
6098
  // Narrow terminals: companion collapses to a one-liner that REPL stacks
6099
  // on its own row (above input in fullscreen, below in scrollback) instead
6100
  // of row-beside. Wide terminals keep the row layout with sprite on the right.
 
6183
  >
6184
  <FullscreenLayout
6185
  scrollRef={scrollRef}
6186
+ overlay={toolPermissionOverlay || questionOverlay}
6187
  bottomFloat={
6188
  feature('BUDDY') && companionVisible && !companionNarrow ? (
6189
  <CompanionFloatingBubble />
src/services/question/questionService.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EventEmitter } from '../../ink/events/emitter.js'
2
+
3
+ export interface QuestionOption {
4
+ label: string
5
+ description?: string
6
+ preview?: string
7
+ }
8
+
9
+ export interface QuestionInfo {
10
+ question: string
11
+ header?: string
12
+ options: QuestionOption[]
13
+ multiSelect?: boolean
14
+ custom?: boolean
15
+ }
16
+
17
+ export interface QuestionRequest {
18
+ id: string
19
+ questions: QuestionInfo[]
20
+ tool?: {
21
+ messageID: string
22
+ callID: string
23
+ }
24
+ }
25
+
26
+ export type QuestionAnswer = string[]
27
+
28
+ export type ReplyInput = {
29
+ requestID: string
30
+ answers: QuestionAnswer[]
31
+ }
32
+
33
+ interface Pending {
34
+ readonly request: QuestionRequest
35
+ resolve: (answers: ReadonlyArray<QuestionAnswer>) => void
36
+ reject: (reason?: Error) => void
37
+ }
38
+
39
+ class RejectedError extends Error {
40
+ constructor() {
41
+ super('The user dismissed this question')
42
+ this.name = 'QuestionV2.RejectedError'
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Standalone question channel, decoupled from the permission system.
48
+ * Mirrors opencode's QuestionV2 service: a tool's `call` awaits `ask`,
49
+ * the TUI renders an independent overlay, and `reply`/`reject` resolve it.
50
+ */
51
+ class QuestionService {
52
+ private pending = new Map<string, Pending>()
53
+ private seq = 0
54
+ readonly events = new EventEmitter()
55
+
56
+ private nextID(): string {
57
+ this.seq += 1
58
+ return `que_${Date.now().toString(36)}_${this.seq}`
59
+ }
60
+
61
+ ask(questions: QuestionInfo[]): Promise<ReadonlyArray<QuestionAnswer>> {
62
+ const id = this.nextID()
63
+ const request: QuestionRequest = { id, questions }
64
+ return new Promise<ReadonlyArray<QuestionAnswer>>((resolve, reject) => {
65
+ this.pending.set(id, { request, resolve, reject })
66
+ this.events.emit('asked', request)
67
+ })
68
+ }
69
+
70
+ reply(input: ReplyInput): boolean {
71
+ const existing = this.pending.get(input.requestID)
72
+ if (!existing) return false
73
+ this.events.emit('replied', {
74
+ requestID: existing.request.id,
75
+ answers: input.answers.map(a => [...a]),
76
+ })
77
+ existing.resolve(input.answers)
78
+ this.pending.delete(input.requestID)
79
+ return true
80
+ }
81
+
82
+ reject(requestID: string): boolean {
83
+ const existing = this.pending.get(requestID)
84
+ if (!existing) return false
85
+ this.events.emit('rejected', { requestID: existing.request.id })
86
+ existing.reject(new RejectedError())
87
+ this.pending.delete(requestID)
88
+ return true
89
+ }
90
+
91
+ list(): QuestionRequest[] {
92
+ return Array.from(this.pending.values(), p => p.request)
93
+ }
94
+
95
+ current(): QuestionRequest | undefined {
96
+ const all = this.list()
97
+ return all[all.length - 1]
98
+ }
99
+ }
100
+
101
+ export const questionService = new QuestionService()
102
+ export { RejectedError }
src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx CHANGED
@@ -10,6 +10,7 @@ import { Box, Text } from '../../ink.js';
10
  import type { Tool } from '../../Tool.js';
11
  import { buildTool, type ToolDef } from '../../Tool.js';
12
  import { lazySchema } from '../../utils/lazySchema.js';
 
13
  import { ASK_USER_QUESTION_TOOL_CHIP_WIDTH, ASK_USER_QUESTION_TOOL_NAME, ASK_USER_QUESTION_TOOL_PROMPT, DESCRIPTION, PREVIEW_FEATURE_PROMPT } from './prompt.js';
14
  const questionOptionSchema = lazySchema(() => z.object({
15
  label: z.string().describe('The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.'),
@@ -181,8 +182,7 @@ export const AskUserQuestionTool: Tool<InputSchema, Output> = buildTool({
181
  },
182
  async checkPermissions(input) {
183
  return {
184
- behavior: 'ask' as const,
185
- message: 'Answer questions?',
186
  updatedInput: input
187
  };
188
  },
@@ -208,9 +208,27 @@ export const AskUserQuestionTool: Tool<InputSchema, Output> = buildTool({
208
  },
209
  async call({
210
  questions,
211
- answers = {},
212
  annotations
213
  }, _context) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  return {
215
  data: {
216
  questions,
 
10
  import type { Tool } from '../../Tool.js';
11
  import { buildTool, type ToolDef } from '../../Tool.js';
12
  import { lazySchema } from '../../utils/lazySchema.js';
13
+ import { questionService, type QuestionInfo } from '../../services/question/questionService.js';
14
  import { ASK_USER_QUESTION_TOOL_CHIP_WIDTH, ASK_USER_QUESTION_TOOL_NAME, ASK_USER_QUESTION_TOOL_PROMPT, DESCRIPTION, PREVIEW_FEATURE_PROMPT } from './prompt.js';
15
  const questionOptionSchema = lazySchema(() => z.object({
16
  label: z.string().describe('The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.'),
 
182
  },
183
  async checkPermissions(input) {
184
  return {
185
+ behavior: 'allow' as const,
 
186
  updatedInput: input
187
  };
188
  },
 
208
  },
209
  async call({
210
  questions,
 
211
  annotations
212
  }, _context) {
213
+ const asked: QuestionInfo[] = questions.map(q => ({
214
+ question: q.question,
215
+ header: q.header,
216
+ options: q.options.map(o => ({
217
+ label: o.label,
218
+ description: o.description,
219
+ preview: o.preview
220
+ })),
221
+ multiSelect: q.multiSelect,
222
+ custom: true
223
+ }))
224
+ const result = await questionService.ask(asked)
225
+ const answers: Record<string, string> = {}
226
+ questions.forEach((q, i) => {
227
+ const answer = result[i]
228
+ if (answer && answer.length > 0) {
229
+ answers[q.question] = q.multiSelect ? answer.join(', ') : answer[0]
230
+ }
231
+ })
232
  return {
233
  data: {
234
  questions,
src/tools/AskUserQuestionTool/prompt.ts CHANGED
@@ -39,6 +39,36 @@ Usage notes:
39
  - Users will always be able to select "Other" to provide custom text input
40
  - Use multiSelect: true to allow multiple answers to be selected for a question
41
  - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  Plan mode note: In plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?" or "Should I proceed?" - use ${EXIT_PLAN_MODE_TOOL_NAME} for plan approval. IMPORTANT: Do not reference "the plan" in your questions (e.g., "Do you have feedback about the plan?", "Does the plan look good?") because the user cannot see the plan in the UI until you call ${EXIT_PLAN_MODE_TOOL_NAME}. If you need plan approval, use ${EXIT_PLAN_MODE_TOOL_NAME} instead.
44
  `
 
39
  - Users will always be able to select "Other" to provide custom text input
40
  - Use multiSelect: true to allow multiple answers to be selected for a question
41
  - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
42
+ - Do NOT add an "Other" option yourself; it is provided automatically.
43
+
44
+ Parameter structure (IMPORTANT):
45
+ - \`questions\` is an array of 1-4 question objects.
46
+ - Each option MUST be an object with a \`label\` (short text) and a \`description\` (what it means). Never pass options as plain strings.
47
+ - Do not add any top-level keys other than \`questions\`.
48
+
49
+ Example (ask two questions at once):
50
+ \`\`\`json
51
+ {
52
+ "questions": [
53
+ {
54
+ "question": "How should image rendering be implemented?",
55
+ "header": "Image",
56
+ "options": [
57
+ { "label": "Terminal inline", "description": "Render images inline in the terminal" },
58
+ { "label": "External viewer", "description": "Open images in an external previewer" }
59
+ ]
60
+ },
61
+ {
62
+ "question": "How should logs be stored?",
63
+ "header": "Logging",
64
+ "options": [
65
+ { "label": "File", "description": "Write logs to a file" },
66
+ { "label": "Database", "description": "Write logs to a database" }
67
+ ]
68
+ }
69
+ ]
70
+ }
71
+ \`\`\`
72
 
73
  Plan mode note: In plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?" or "Should I proceed?" - use ${EXIT_PLAN_MODE_TOOL_NAME} for plan approval. IMPORTANT: Do not reference "the plan" in your questions (e.g., "Do you have feedback about the plan?", "Does the plan look good?") because the user cannot see the plan in the UI until you call ${EXIT_PLAN_MODE_TOOL_NAME}. If you need plan approval, use ${EXIT_PLAN_MODE_TOOL_NAME} instead.
74
  `