@ryan_nookpi/pi-extension-ask-user-question 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -16
- package/constants.ts +10 -0
- package/controller.test.ts +281 -0
- package/controller.ts +280 -0
- package/form-ui.test.ts +57 -0
- package/form-ui.ts +38 -0
- package/index.test.ts +113 -0
- package/index.ts +62 -510
- package/output.test.ts +102 -0
- package/output.ts +89 -0
- package/package.json +2 -2
- package/schema.ts +37 -0
- package/state.test.ts +136 -0
- package/state.ts +131 -0
- package/types.ts +85 -0
- package/view.test.ts +337 -0
- package/view.ts +218 -0
package/index.ts
CHANGED
|
@@ -1,533 +1,85 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const OTHER_OPTION_LABEL = "Other (type your own)";
|
|
35
|
-
const DONE_OPTION_LABEL = "Done selecting";
|
|
36
|
-
|
|
37
|
-
function stripMarkdownForDisplay(text: string): string {
|
|
38
|
-
return text
|
|
39
|
-
.replace(/\r\n/g, "\n")
|
|
40
|
-
.replace(/^\s{0,3}#{1,6}\s+/gm, "")
|
|
41
|
-
.replace(/^\s{0,3}>\s?/gm, "")
|
|
42
|
-
.replace(/^\s*[-*+]\s+/gm, "• ")
|
|
43
|
-
.replace(/^\s*\d+\.\s+/gm, "• ")
|
|
44
|
-
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1")
|
|
45
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)")
|
|
46
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
47
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
48
|
-
.replace(/__([^_]+)__/g, "$1")
|
|
49
|
-
.replace(/~~([^~]+)~~/g, "$1")
|
|
50
|
-
.replace(/(^|[^*])\*([^*\n]+)\*(?=[^*]|$)/g, "$1$2")
|
|
51
|
-
.replace(/(^|[^_])_([^_\n]+)_(?=[^_]|$)/g, "$1$2")
|
|
52
|
-
.replace(/^\s*([-*_]\s*){3,}$/gm, "")
|
|
53
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
54
|
-
.trim();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function toDisplayText(text: string): string {
|
|
58
|
-
const stripped = stripMarkdownForDisplay(text).trim();
|
|
59
|
-
return stripped || text.trim() || "(empty)";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildDisplayOptions(options: string[]): string[] {
|
|
63
|
-
const base = options.map((option) => toDisplayText(option));
|
|
64
|
-
const counts = new Map<string, number>();
|
|
65
|
-
for (const label of base) counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
66
|
-
return base.map((label, index) => ((counts.get(label) ?? 0) > 1 ? `${index + 1}. ${label}` : label));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeOptions(raw: unknown): string[] {
|
|
70
|
-
if (!Array.isArray(raw)) return [];
|
|
71
|
-
const dedup = new Set<string>();
|
|
72
|
-
for (const item of raw) {
|
|
73
|
-
if (typeof item !== "string") continue;
|
|
74
|
-
const normalized = item.trim();
|
|
75
|
-
if (!normalized) continue;
|
|
76
|
-
dedup.add(normalized);
|
|
77
|
-
}
|
|
78
|
-
return Array.from(dedup);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function buildPrompt(question: string, context?: string): string {
|
|
82
|
-
const normalizedQuestion = toDisplayText(question);
|
|
83
|
-
const ctx = typeof context === "string" ? toDisplayText(context) : "";
|
|
84
|
-
if (!ctx) return normalizedQuestion;
|
|
85
|
-
return `${normalizedQuestion}\n\n${ctx}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function clampLines(text: string, maxLines: number): string {
|
|
89
|
-
const lines = text.split("\n");
|
|
90
|
-
if (lines.length <= maxLines) return text;
|
|
91
|
-
const hidden = lines.length - maxLines;
|
|
92
|
-
const visible = lines.slice(0, maxLines);
|
|
93
|
-
const lastIndex = visible.length - 1;
|
|
94
|
-
visible[lastIndex] = `${visible[lastIndex]} … (+${hidden} lines)`;
|
|
95
|
-
return visible.join("\n");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function buildDetails(
|
|
99
|
-
base: Omit<AskUserQuestionDetails, "answer" | "answers" | "cancelled"> & {
|
|
100
|
-
answer?: string | null;
|
|
101
|
-
answers?: string[];
|
|
102
|
-
cancelled?: boolean;
|
|
103
|
-
},
|
|
104
|
-
): AskUserQuestionDetails {
|
|
105
|
-
return {
|
|
106
|
-
...base,
|
|
107
|
-
answer: base.answer ?? null,
|
|
108
|
-
answers: base.answers ?? [],
|
|
109
|
-
cancelled: base.cancelled ?? false,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function buildCancelledResult(
|
|
114
|
-
question: string,
|
|
115
|
-
context: string | undefined,
|
|
116
|
-
options: string[],
|
|
117
|
-
allowCustomAnswer: boolean,
|
|
118
|
-
allowMultiple: boolean,
|
|
119
|
-
) {
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { runAskUserQuestionForm } from "./form-ui.ts";
|
|
4
|
+
import {
|
|
5
|
+
buildRenderCallText,
|
|
6
|
+
buildRenderResultText,
|
|
7
|
+
errorResult,
|
|
8
|
+
formatResultContent,
|
|
9
|
+
renderCall,
|
|
10
|
+
renderResult,
|
|
11
|
+
} from "./output.ts";
|
|
12
|
+
import { AskUserQuestionParams } from "./schema.ts";
|
|
13
|
+
import { normalizeQuestions } from "./state.ts";
|
|
14
|
+
import type { AskUserQuestionParamsInput, FormResult, Question } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
const TOOL_DESCRIPTION = `Ask the user one or more questions using an interactive form. Supports three question types:
|
|
17
|
+
- **radio**: Single-select from predefined options (like multiple choice)
|
|
18
|
+
- **checkbox**: Multi-select from options (pick all that apply)
|
|
19
|
+
- **text**: Free-form text input
|
|
20
|
+
|
|
21
|
+
Each radio/checkbox question can include an "Other..." option that lets the user type a custom answer.
|
|
22
|
+
|
|
23
|
+
Use this tool when you need user input to proceed — for clarifying requirements, getting preferences, confirming decisions, or choosing between alternatives. Prefer this over asking plain-text questions in your response.`;
|
|
24
|
+
|
|
25
|
+
const PROMPT_GUIDELINES = [
|
|
26
|
+
"Use ask_user_question instead of asking questions in plain text when you need structured user input.",
|
|
27
|
+
"Prefer radio for single-choice, checkbox for multi-choice, text for open-ended answers.",
|
|
28
|
+
"Always include an 'Other' escape hatch (allowOther: true) unless the options are exhaustive.",
|
|
29
|
+
"Group related questions in a single call rather than making multiple separate calls.",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function buildCancelledResponse(result: FormResult) {
|
|
120
33
|
return {
|
|
121
|
-
content: [{ type: "text" as const, text: "User cancelled
|
|
122
|
-
details:
|
|
123
|
-
question,
|
|
124
|
-
context,
|
|
125
|
-
options,
|
|
126
|
-
allowCustomAnswer,
|
|
127
|
-
allowMultiple,
|
|
128
|
-
cancelled: true,
|
|
129
|
-
}),
|
|
34
|
+
content: [{ type: "text" as const, text: "User cancelled the form" }],
|
|
35
|
+
details: result,
|
|
130
36
|
};
|
|
131
37
|
}
|
|
132
38
|
|
|
133
|
-
function
|
|
134
|
-
return {
|
|
135
|
-
question: typeof params.question === "string" ? params.question.trim() : "",
|
|
136
|
-
context: typeof params.context === "string" ? params.context.trim() : undefined,
|
|
137
|
-
options: normalizeOptions(params.options),
|
|
138
|
-
allowCustomAnswer: typeof params.allowCustomAnswer === "boolean" ? params.allowCustomAnswer : true,
|
|
139
|
-
allowMultiple: typeof params.allowMultiple === "boolean" ? params.allowMultiple : false,
|
|
140
|
-
placeholder: typeof params.placeholder === "string" ? params.placeholder : "",
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function buildAskUserQuestionErrorResult(message: string, params: AskUserQuestionExecutionParams) {
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: "text" as const, text: message }],
|
|
147
|
-
details: buildDetails({
|
|
148
|
-
question: params.question,
|
|
149
|
-
context: params.context,
|
|
150
|
-
options: params.options,
|
|
151
|
-
allowCustomAnswer: params.allowCustomAnswer,
|
|
152
|
-
allowMultiple: params.allowMultiple,
|
|
153
|
-
cancelled: true,
|
|
154
|
-
}),
|
|
155
|
-
isError: true,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function askSingleChoice(
|
|
160
|
-
ctx: ExtensionContext,
|
|
161
|
-
params: AskUserQuestionExecutionParams,
|
|
162
|
-
): Promise<AskSingleChoiceResult | undefined> {
|
|
163
|
-
if (params.options.length > 0) {
|
|
164
|
-
const displayOptions = buildDisplayOptions(params.options);
|
|
165
|
-
const selectable = params.allowCustomAnswer ? [...displayOptions, OTHER_OPTION_LABEL] : [...displayOptions];
|
|
166
|
-
const selected = await ctx.ui.select(buildPrompt(params.question, params.context), selectable);
|
|
167
|
-
if (selected === undefined) return undefined;
|
|
168
|
-
if (selected === OTHER_OPTION_LABEL) {
|
|
169
|
-
const customInput = await promptForCustomInput(ctx, params.placeholder);
|
|
170
|
-
if (customInput === undefined) return undefined;
|
|
171
|
-
return {
|
|
172
|
-
answer: customInput,
|
|
173
|
-
selectedOption: "custom",
|
|
174
|
-
customInput,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const optionIndex = displayOptions.indexOf(selected);
|
|
179
|
-
return {
|
|
180
|
-
answer: optionIndex >= 0 ? params.options[optionIndex] : selected,
|
|
181
|
-
selectedOption: optionIndex >= 0 ? params.options[optionIndex] : selected,
|
|
182
|
-
selectedIndex: optionIndex >= 0 ? optionIndex + 1 : undefined,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const answer = await ctx.ui.input(buildPrompt(params.question, params.context), params.placeholder);
|
|
187
|
-
if (answer === undefined) return undefined;
|
|
188
|
-
return {
|
|
189
|
-
answer,
|
|
190
|
-
customInput: answer.trim() || undefined,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
type MultiSelectResult = {
|
|
195
|
-
cancelled: boolean;
|
|
196
|
-
answers: string[];
|
|
197
|
-
selectedIndices: number[];
|
|
198
|
-
customInput?: string;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
type AskSingleChoiceResult = {
|
|
202
|
-
answer: string;
|
|
203
|
-
selectedOption?: string;
|
|
204
|
-
selectedIndex?: number;
|
|
205
|
-
customInput?: string;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
type AskUserQuestionExecutionParams = {
|
|
209
|
-
question: string;
|
|
210
|
-
context?: string;
|
|
211
|
-
options: string[];
|
|
212
|
-
allowCustomAnswer: boolean;
|
|
213
|
-
allowMultiple: boolean;
|
|
214
|
-
placeholder: string;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
type OptionEntry =
|
|
218
|
-
| { kind: "option"; label: string; optionIndex: number }
|
|
219
|
-
| { kind: "custom"; label: string; customIndex: number }
|
|
220
|
-
| { kind: "other"; label: string }
|
|
221
|
-
| { kind: "done"; label: string };
|
|
222
|
-
|
|
223
|
-
async function promptForCustomInput(ctx: ExtensionContext, placeholder: string): Promise<string | undefined> {
|
|
224
|
-
const answer = await ctx.ui.input("Your answer", placeholder);
|
|
225
|
-
if (answer === undefined) return undefined;
|
|
226
|
-
const normalized = answer.trim();
|
|
227
|
-
if (!normalized) {
|
|
228
|
-
ctx.ui.notify("Empty custom answer ignored.", "warning");
|
|
229
|
-
return "";
|
|
230
|
-
}
|
|
231
|
-
return normalized;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function buildMultiSelectEntries(
|
|
235
|
-
displayOptions: string[],
|
|
236
|
-
selectedOptionIndices: Set<number>,
|
|
237
|
-
allowCustomAnswer: boolean,
|
|
238
|
-
customAnswers: string[],
|
|
239
|
-
): OptionEntry[] {
|
|
240
|
-
const entries: OptionEntry[] = displayOptions.map((option, index) => ({
|
|
241
|
-
kind: "option",
|
|
242
|
-
optionIndex: index,
|
|
243
|
-
label: `${selectedOptionIndices.has(index) ? "☑" : "☐"} ${index + 1}. ${option}`,
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
if (!allowCustomAnswer) {
|
|
247
|
-
return entries;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
entries.push({ kind: "other", label: OTHER_OPTION_LABEL });
|
|
251
|
-
customAnswers.forEach((answer, index) => {
|
|
252
|
-
entries.push({ kind: "custom", customIndex: index, label: `☑ custom ${index + 1}. ${answer}` });
|
|
253
|
-
});
|
|
254
|
-
return entries;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function buildSelectedSummary(
|
|
258
|
-
displayOptions: string[],
|
|
259
|
-
selectedOptionIndices: Set<number>,
|
|
260
|
-
customAnswers: string[],
|
|
261
|
-
): string[] {
|
|
262
|
-
return [
|
|
263
|
-
...Array.from(selectedOptionIndices)
|
|
264
|
-
.sort((a, b) => a - b)
|
|
265
|
-
.map((index) => displayOptions[index]),
|
|
266
|
-
...customAnswers,
|
|
267
|
-
];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function findSelectedEntry(entries: OptionEntry[], choice: string): OptionEntry | undefined {
|
|
271
|
-
return entries.find((entry) => entry.label === choice);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function finalizeMultiSelect(
|
|
275
|
-
ctx: ExtensionContext,
|
|
276
|
-
options: string[],
|
|
277
|
-
selectedOptionIndices: Set<number>,
|
|
278
|
-
customAnswers: string[],
|
|
279
|
-
): MultiSelectResult | null {
|
|
280
|
-
const answers = [
|
|
281
|
-
...Array.from(selectedOptionIndices)
|
|
282
|
-
.sort((a, b) => a - b)
|
|
283
|
-
.map((index) => options[index]),
|
|
284
|
-
...customAnswers,
|
|
285
|
-
];
|
|
286
|
-
if (answers.length === 0) {
|
|
287
|
-
ctx.ui.notify("Select at least one option before finishing.", "warning");
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
39
|
+
function buildSuccessResponse(result: FormResult) {
|
|
290
40
|
return {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
selectedIndices: Array.from(selectedOptionIndices)
|
|
294
|
-
.sort((a, b) => a - b)
|
|
295
|
-
.map((index) => index + 1),
|
|
296
|
-
customInput: customAnswers.length > 0 ? customAnswers.join(", ") : undefined,
|
|
41
|
+
content: [{ type: "text" as const, text: formatResultContent(result) }],
|
|
42
|
+
details: result,
|
|
297
43
|
};
|
|
298
44
|
}
|
|
299
45
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
entry: OptionEntry,
|
|
303
|
-
selectedOptionIndices: Set<number>,
|
|
304
|
-
customAnswers: string[],
|
|
305
|
-
options: string[],
|
|
306
|
-
placeholder: string,
|
|
307
|
-
): Promise<MultiSelectResult | null> {
|
|
308
|
-
if (entry.kind === "option") {
|
|
309
|
-
if (selectedOptionIndices.has(entry.optionIndex)) selectedOptionIndices.delete(entry.optionIndex);
|
|
310
|
-
else selectedOptionIndices.add(entry.optionIndex);
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (entry.kind === "custom") {
|
|
315
|
-
customAnswers.splice(entry.customIndex, 1);
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
46
|
+
export type { AskUserQuestionParamsInput, FormResult, Question };
|
|
47
|
+
export { AskUserQuestionParams, buildRenderCallText, buildRenderResultText, errorResult, normalizeQuestions };
|
|
318
48
|
|
|
319
|
-
|
|
320
|
-
const customAnswer = await promptForCustomInput(ctx, placeholder);
|
|
321
|
-
if (customAnswer && !customAnswers.includes(customAnswer)) {
|
|
322
|
-
customAnswers.push(customAnswer);
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return finalizeMultiSelect(ctx, options, selectedOptionIndices, customAnswers);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function askMultipleOptions(
|
|
331
|
-
ctx: ExtensionContext,
|
|
332
|
-
question: string,
|
|
333
|
-
context: string | undefined,
|
|
334
|
-
options: string[],
|
|
335
|
-
allowCustomAnswer: boolean,
|
|
336
|
-
placeholder: string,
|
|
337
|
-
): Promise<MultiSelectResult> {
|
|
338
|
-
const selectedOptionIndices = new Set<number>();
|
|
339
|
-
const customAnswers: string[] = [];
|
|
340
|
-
const displayOptions = options.map((option) => toDisplayText(option));
|
|
341
|
-
|
|
342
|
-
while (true) {
|
|
343
|
-
const selectedSummary = buildSelectedSummary(displayOptions, selectedOptionIndices, customAnswers);
|
|
344
|
-
const entries = buildMultiSelectEntries(displayOptions, selectedOptionIndices, allowCustomAnswer, customAnswers);
|
|
345
|
-
entries.push({ kind: "done", label: `${DONE_OPTION_LABEL} (${selectedSummary.length} selected)` });
|
|
346
|
-
|
|
347
|
-
const choice = await ctx.ui.select(
|
|
348
|
-
`${buildPrompt(question, context)}\n\nSelected: ${selectedSummary.length > 0 ? selectedSummary.join(", ") : "(none)"}`,
|
|
349
|
-
entries.map((entry) => entry.label),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
if (choice === undefined) {
|
|
353
|
-
return { cancelled: true, answers: [], selectedIndices: [] };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const selectedEntry = findSelectedEntry(entries, choice);
|
|
357
|
-
if (!selectedEntry) continue;
|
|
358
|
-
|
|
359
|
-
const result = await handleMultiSelectEntry(
|
|
360
|
-
ctx,
|
|
361
|
-
selectedEntry,
|
|
362
|
-
selectedOptionIndices,
|
|
363
|
-
customAnswers,
|
|
364
|
-
options,
|
|
365
|
-
placeholder,
|
|
366
|
-
);
|
|
367
|
-
if (result) {
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export default function askUserQuestionExtension(pi: ExtensionAPI) {
|
|
49
|
+
export default function askUserQuestion(pi: ExtensionAPI) {
|
|
374
50
|
pi.registerTool({
|
|
375
|
-
name: "
|
|
376
|
-
label: "
|
|
377
|
-
description:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"2. Clarify ambiguous instructions\n" +
|
|
381
|
-
"3. Get decisions on implementation choices as you work\n" +
|
|
382
|
-
"4. Offer choices to the user about what direction to take\n\n" +
|
|
383
|
-
"Usage notes:\n" +
|
|
384
|
-
'- Users will always be able to select "Other" to provide custom text input\n' +
|
|
385
|
-
"- Use allowMultiple: true to allow multiple answers to be selected for a question\n" +
|
|
386
|
-
'- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label\n' +
|
|
387
|
-
'- Do NOT use this tool to ask "Should I proceed?" or seek unnecessary confirmation — just proceed with the task',
|
|
51
|
+
name: "ask_user_question",
|
|
52
|
+
label: "Ask User",
|
|
53
|
+
description: TOOL_DESCRIPTION,
|
|
54
|
+
promptSnippet: "Ask the user interactive questions with radio, checkbox, or text inputs",
|
|
55
|
+
promptGuidelines: PROMPT_GUIDELINES,
|
|
388
56
|
parameters: AskUserQuestionParams,
|
|
389
|
-
|
|
390
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
|
|
391
|
-
const executionParams = buildExecutionParams(params as Record<string, unknown>);
|
|
392
|
-
const { question, context, options, allowCustomAnswer, allowMultiple } = executionParams;
|
|
393
|
-
|
|
394
|
-
if (!question) {
|
|
395
|
-
return buildAskUserQuestionErrorResult("AskUserQuestion requires a non-empty question.", executionParams);
|
|
396
|
-
}
|
|
397
|
-
|
|
57
|
+
async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
398
58
|
if (!ctx.hasUI) {
|
|
399
|
-
return
|
|
400
|
-
"AskUserQuestion requires interactive mode (UI unavailable).",
|
|
401
|
-
executionParams,
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
ctx.ui.notify("Waiting for input", "info");
|
|
406
|
-
|
|
407
|
-
if (allowMultiple && options.length > 0) {
|
|
408
|
-
const multipleResult = await askMultipleOptions(
|
|
409
|
-
ctx,
|
|
410
|
-
question,
|
|
411
|
-
context,
|
|
412
|
-
options,
|
|
413
|
-
allowCustomAnswer,
|
|
414
|
-
executionParams.placeholder,
|
|
415
|
-
);
|
|
416
|
-
if (multipleResult.cancelled) {
|
|
417
|
-
return buildCancelledResult(question, context, options, allowCustomAnswer, allowMultiple);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const answerText = multipleResult.answers.join(", ");
|
|
421
|
-
return {
|
|
422
|
-
content: [{ type: "text" as const, text: answerText }],
|
|
423
|
-
details: buildDetails({
|
|
424
|
-
question,
|
|
425
|
-
context,
|
|
426
|
-
options,
|
|
427
|
-
allowCustomAnswer,
|
|
428
|
-
allowMultiple,
|
|
429
|
-
answer: answerText,
|
|
430
|
-
answers: multipleResult.answers,
|
|
431
|
-
selectedOptions: multipleResult.answers,
|
|
432
|
-
selectedIndices: multipleResult.selectedIndices,
|
|
433
|
-
customInput: multipleResult.customInput,
|
|
434
|
-
}),
|
|
435
|
-
};
|
|
59
|
+
return errorResult("Error: UI not available (running in non-interactive mode)");
|
|
436
60
|
}
|
|
437
61
|
|
|
438
|
-
const
|
|
439
|
-
if (!
|
|
440
|
-
return
|
|
62
|
+
const params = rawParams as AskUserQuestionParamsInput;
|
|
63
|
+
if (!params.questions.length) {
|
|
64
|
+
return errorResult("Error: No questions provided");
|
|
441
65
|
}
|
|
442
66
|
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
allowCustomAnswer,
|
|
451
|
-
allowMultiple,
|
|
452
|
-
answer: normalizedAnswer,
|
|
453
|
-
answers: [normalizedAnswer],
|
|
454
|
-
selectedOption: singleChoice.selectedOption,
|
|
455
|
-
selectedOptions:
|
|
456
|
-
singleChoice.selectedOption && singleChoice.selectedOption !== "custom"
|
|
457
|
-
? [singleChoice.selectedOption]
|
|
458
|
-
: normalizedAnswer
|
|
459
|
-
? [normalizedAnswer]
|
|
460
|
-
: [],
|
|
461
|
-
selectedIndex: singleChoice.selectedIndex,
|
|
462
|
-
selectedIndices: singleChoice.selectedIndex ? [singleChoice.selectedIndex] : undefined,
|
|
463
|
-
customInput: singleChoice.selectedOption === "custom" ? normalizedAnswer : singleChoice.customInput,
|
|
464
|
-
}),
|
|
465
|
-
};
|
|
67
|
+
const questions = normalizeQuestions(params.questions as Question[]);
|
|
68
|
+
const result = await runAskUserQuestionForm(
|
|
69
|
+
ctx,
|
|
70
|
+
{ title: params.title, description: params.description },
|
|
71
|
+
questions,
|
|
72
|
+
);
|
|
73
|
+
return result.cancelled ? buildCancelledResponse(result) : buildSuccessResponse(result);
|
|
466
74
|
},
|
|
467
|
-
|
|
468
75
|
renderCall(args, theme) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const allowMultiple = args.allowMultiple ?? false;
|
|
474
|
-
|
|
475
|
-
let text = `${theme.fg("toolTitle", theme.bold("AskUserQuestion"))} ${theme.fg("accent", question)}`;
|
|
476
|
-
if (options.length > 0) {
|
|
477
|
-
const renderedOptions = allowCustomAnswer ? [...options, "Other"] : options;
|
|
478
|
-
text += `\n${theme.fg("dim", `options:${renderedOptions.length}${allowMultiple ? " · multi" : ""}`)}`;
|
|
479
|
-
for (let i = 0; i < Math.min(renderedOptions.length, 4); i++) {
|
|
480
|
-
text += `\n${theme.fg("muted", `- ${renderedOptions[i]}`)}`;
|
|
481
|
-
}
|
|
482
|
-
if (renderedOptions.length > 4) {
|
|
483
|
-
text += `\n${theme.fg("muted", `… ${renderedOptions.length - 4} more`)}`;
|
|
484
|
-
}
|
|
485
|
-
} else if (context) {
|
|
486
|
-
text += `\n${theme.fg("muted", context)}`;
|
|
487
|
-
}
|
|
488
|
-
return new Text(clampLines(text, 6), 0, 0);
|
|
76
|
+
return renderCall(
|
|
77
|
+
{ questions: args.questions as Question[] | undefined, title: args.title as string | undefined },
|
|
78
|
+
theme,
|
|
79
|
+
);
|
|
489
80
|
},
|
|
490
|
-
|
|
491
81
|
renderResult(result, _options, theme) {
|
|
492
|
-
|
|
493
|
-
if (!details) {
|
|
494
|
-
const text = result.content[0];
|
|
495
|
-
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (details.cancelled) {
|
|
499
|
-
return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (details.allowMultiple) {
|
|
503
|
-
const answers = details.answers?.filter((answer) => answer?.trim()) ?? [];
|
|
504
|
-
let text =
|
|
505
|
-
theme.fg("success", "✓ ") +
|
|
506
|
-
theme.fg("muted", `selected ${answers.length}: `) +
|
|
507
|
-
theme.fg("accent", answers.length > 0 ? answers.join(", ") : "(none)");
|
|
508
|
-
if (details.customInput) {
|
|
509
|
-
text += `\n${theme.fg("dim", `custom: ${details.customInput}`)}`;
|
|
510
|
-
}
|
|
511
|
-
return new Text(clampLines(text, 3), 0, 0);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const answerText = details.answer ?? "";
|
|
515
|
-
if (details.selectedOption && details.selectedOption !== "custom") {
|
|
516
|
-
const indexPrefix = details.selectedIndex ? `${details.selectedIndex}. ` : "";
|
|
517
|
-
return new Text(
|
|
518
|
-
theme.fg("success", "✓ ") +
|
|
519
|
-
theme.fg("muted", "selected ") +
|
|
520
|
-
theme.fg("accent", `${indexPrefix}${answerText}`),
|
|
521
|
-
0,
|
|
522
|
-
0,
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return new Text(
|
|
527
|
-
theme.fg("success", "✓ ") + theme.fg("muted", "answered ") + theme.fg("accent", answerText || "(empty answer)"),
|
|
528
|
-
0,
|
|
529
|
-
0,
|
|
530
|
-
);
|
|
82
|
+
return renderResult(result as { content?: Array<{ type: string; text?: string }>; details?: FormResult }, theme);
|
|
531
83
|
},
|
|
532
84
|
});
|
|
533
85
|
}
|