@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/index.ts CHANGED
@@ -1,533 +1,85 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { Text } from "@mariozechner/pi-tui";
3
- import { Type } from "@sinclair/typebox";
4
-
5
- interface AskUserQuestionDetails {
6
- question: string;
7
- context?: string;
8
- options: string[];
9
- allowCustomAnswer: boolean;
10
- allowMultiple: boolean;
11
- answer: string | null;
12
- answers: string[];
13
- selectedOption?: string;
14
- selectedOptions?: string[];
15
- selectedIndex?: number;
16
- selectedIndices?: number[];
17
- customInput?: string;
18
- cancelled: boolean;
19
- }
20
-
21
- const AskUserQuestionParams = Type.Object({
22
- question: Type.String({ description: "Question to ask the user" }),
23
- context: Type.Optional(Type.String({ description: "Optional extra context shown to the user" })),
24
- options: Type.Optional(Type.Array(Type.String(), { description: "Optional predefined options" })),
25
- allowCustomAnswer: Type.Optional(
26
- Type.Boolean({ description: "If true, allow typing a custom answer when options are provided", default: true }),
27
- ),
28
- allowMultiple: Type.Optional(
29
- Type.Boolean({ description: "If true, allow selecting multiple options", default: false }),
30
- ),
31
- placeholder: Type.Optional(Type.String({ description: "Optional input placeholder for typed answers" })),
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 AskUserQuestion." }],
122
- details: buildDetails({
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 buildExecutionParams(params: Record<string, unknown>): AskUserQuestionExecutionParams {
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
- cancelled: false,
292
- answers,
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
- async function handleMultiSelectEntry(
301
- ctx: ExtensionContext,
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
- if (entry.kind === "other") {
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: "AskUserQuestion",
376
- label: "AskUserQuestion",
377
- description:
378
- "Ask the user a question and wait for their response. Use this when you need to:\n" +
379
- "1. Gather user preferences or requirements\n" +
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 buildAskUserQuestionErrorResult(
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 singleChoice = await askSingleChoice(ctx, executionParams);
439
- if (!singleChoice) {
440
- return buildCancelledResult(question, context, options, allowCustomAnswer, allowMultiple);
62
+ const params = rawParams as AskUserQuestionParamsInput;
63
+ if (!params.questions.length) {
64
+ return errorResult("Error: No questions provided");
441
65
  }
442
66
 
443
- const normalizedAnswer = singleChoice.answer.trim();
444
- return {
445
- content: [{ type: "text" as const, text: normalizedAnswer || "(empty answer)" }],
446
- details: buildDetails({
447
- question,
448
- context,
449
- options,
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
- const question = typeof args.question === "string" ? toDisplayText(args.question) : "(no question)";
470
- const context = typeof args.context === "string" && args.context.trim() ? toDisplayText(args.context) : "";
471
- const options = buildDisplayOptions(normalizeOptions(args.options));
472
- const allowCustomAnswer = args.allowCustomAnswer ?? true;
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
- const details = result.details as AskUserQuestionDetails | undefined;
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
  }