killeros 3.0.6 → 3.0.8

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.
@@ -1,242 +1,242 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import {
3
- type ExtensionAPI,
4
- type ToolDefinition,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { Type, type Static } from "typebox";
7
- import { BoundedText } from "./bounded-text.ts";
8
- import { CUSTOM_INPUT_HISTORY_BYTES, CUSTOM_INPUT_HISTORY_LIMIT, FILTER_QUERY_MAX_BYTES, FILTER_QUERY_MAX_CHARACTERS, MultipleResultText, oneLine, openQuestionUi, type DisplayOption } from "./question-ui.ts";
9
- import { safeTerminalText } from "./safe-terminal-text.ts";
10
-
11
- const OptionSchema = Type.Object({
12
- label: Type.String({ minLength: 1, maxLength: 200, description: "Display label for the option" }),
13
- description: Type.Optional(Type.String({ maxLength: 500, description: "Optional detail shown for the selected option" })),
14
- preview: Type.Optional(Type.String({ maxLength: 8_000, description: "Optional markdown proposal preview shown for the selected option" })),
15
- });
16
-
17
- const QuestionParams = Type.Object({
18
- question: Type.String({ minLength: 1, maxLength: 1_000, description: "The question to ask the user" }),
19
- options: Type.Array(OptionSchema, {
20
- minItems: 1,
21
- maxItems: 9,
22
- description: "Between 1 and 9 options for the user to choose from",
23
- }),
24
- mode: Type.Optional(StringEnum(["single", "multiple"] as const, {
25
- description: "Choose one answer or multiple answers; defaults to single",
26
- })),
27
- minSelections: Type.Optional(Type.Integer({
28
- minimum: 1,
29
- maximum: 10,
30
- description: "Minimum answers required in multiple mode; defaults to 1. Single mode accepts only an explicit 1/1 bounds pair",
31
- })),
32
- maxSelections: Type.Optional(Type.Integer({
33
- minimum: 1,
34
- maximum: 10,
35
- description: "Maximum answers allowed in multiple mode; defaults to all options plus one custom answer. Single mode accepts only an explicit 1/1 bounds pair",
36
- })),
37
- });
38
-
39
- type QuestionParamsValue = Static<typeof QuestionParams>;
40
-
41
- type NormalizedQuestionSelection =
42
- | { mode: "single"; minSelections: 1; maxSelections: 1 }
43
- | { mode: "multiple"; minSelections: number; maxSelections: number };
44
-
45
- function normalizeQuestionSelection(params: QuestionParamsValue): NormalizedQuestionSelection {
46
- const mode = params.mode ?? "single";
47
- if (mode === "single") {
48
- const hasSelectionBounds = params.minSelections !== undefined || params.maxSelections !== undefined;
49
- if (hasSelectionBounds && (params.minSelections !== 1 || params.maxSelections !== 1)) {
50
- throw new Error("Single-select question bounds must be omitted or both be 1");
51
- }
52
- return { mode, minSelections: 1, maxSelections: 1 };
53
- }
54
-
55
- const maximumAvailable = params.options.length + 1;
56
- const minSelections = params.minSelections ?? 1;
57
- const maxSelections = params.maxSelections ?? maximumAvailable;
58
- if (minSelections > maxSelections) {
59
- throw new Error("Question minimum selections cannot exceed maximum selections");
60
- }
61
- if (maxSelections > maximumAvailable) {
62
- throw new Error(`Question allows at most ${maximumAvailable} selections including one custom answer`);
63
- }
64
- return { mode, minSelections, maxSelections };
65
- }
66
-
67
- interface SingleQuestionDetails {
68
- question: string;
69
- options: string[];
70
- answer: string | null;
71
- selectedIndex?: number;
72
- wasCustom?: boolean;
73
- cancelled?: boolean;
74
- }
75
-
76
- interface MultipleQuestionDetails {
77
- question: string;
78
- options: string[];
79
- mode: "multiple";
80
- answers: string[];
81
- selectedIndices: number[];
82
- customAnswer?: string;
83
- cancelled?: boolean;
84
- }
85
-
86
- type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
87
-
88
- export function registerQuestionTool(pi: ExtensionAPI): void {
89
- const customInputHistory: string[] = [];
90
- const pendingQuestions = new Set<AbortController>();
91
- let customInputHistoryBytes = 0;
92
- const clearCustomInputHistory = (): void => {
93
- for (const pending of pendingQuestions) pending.abort();
94
- customInputHistory.length = 0;
95
- customInputHistoryBytes = 0;
96
- };
97
- const rememberCustomInput = (value: string): boolean => {
98
- const bytes = Buffer.byteLength(value, "utf8");
99
- if (bytes > CUSTOM_INPUT_HISTORY_BYTES) return false;
100
- const existingIndex = customInputHistory.indexOf(value);
101
- if (existingIndex >= 0) {
102
- const existing = customInputHistory[existingIndex];
103
- if (existing !== undefined) customInputHistoryBytes -= Buffer.byteLength(existing, "utf8");
104
- customInputHistory.splice(existingIndex, 1);
105
- }
106
- while (customInputHistory.length >= CUSTOM_INPUT_HISTORY_LIMIT || customInputHistoryBytes + bytes > CUSTOM_INPUT_HISTORY_BYTES) {
107
- const removed = customInputHistory.shift();
108
- if (removed !== undefined) customInputHistoryBytes -= Buffer.byteLength(removed, "utf8");
109
- }
110
- customInputHistory.push(value);
111
- customInputHistoryBytes += bytes;
112
- return true;
113
- };
114
- pi.on("session_start", clearCustomInputHistory);
115
- pi.on("session_tree", clearCustomInputHistory);
116
- pi.on("session_shutdown", clearCustomInputHistory);
117
-
118
- const questionTool: ToolDefinition<typeof QuestionParams, QuestionDetails> = {
119
- name: "question",
120
- label: "Question",
121
- description: `Ask one interactive multiple-choice question. Provide 1-9 concise options. Single-select is the default; opt into bounded multi-select with mode "multiple". The user can filter options or type a custom answer. Filter queries are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes.`,
122
- promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
123
- promptGuidelines: [
124
- "Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
125
- "Use multiple mode only when the user may need to choose more than one answer; ordinary either/or decisions remain single-select.",
126
- ],
127
- parameters: QuestionParams,
128
- exposure: "model-only",
129
- executionMode: "sequential",
130
-
131
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
132
- const { mode, minSelections, maxSelections } = normalizeQuestionSelection(params);
133
- if (ctx.mode !== "tui") throw new Error("The question tool requires interactive TUI mode");
134
- if (signal?.aborted) throw new Error("Question cancelled before it opened");
135
-
136
- const question = safeTerminalText(params.question);
137
- const options: DisplayOption[] = params.options.map((option, index) => ({
138
- label: safeTerminalText(option.label),
139
- answer: option.label,
140
- description: option.description === undefined ? undefined : safeTerminalText(option.description),
141
- preview: option.preview === undefined ? undefined : safeTerminalText(option.preview),
142
- originalIndex: index + 1,
143
- }));
144
-
145
- const pending = new AbortController();
146
- pendingQuestions.add(pending);
147
- let result;
148
- try {
149
- result = await openQuestionUi({
150
- ctx,
151
- signal: signal ? AbortSignal.any([signal, pending.signal]) : pending.signal,
152
- question,
153
- options,
154
- mode,
155
- minSelections,
156
- maxSelections,
157
- customInputHistory,
158
- rememberCustomInput,
159
- });
160
- } finally {
161
- pendingQuestions.delete(pending);
162
- }
163
-
164
- const simpleOptions = params.options.map((option) => option.label);
165
- if (result.kind === "aborted" || pending.signal.aborted || signal?.aborted) {
166
- throw new Error("Question cancelled because the agent operation was aborted");
167
- }
168
- if (result.kind === "cancelled" && mode === "multiple") {
169
- return {
170
- content: [{ type: "text", text: "User cancelled the question" }],
171
- details: { question: params.question, options: simpleOptions, mode: "multiple", answers: [], selectedIndices: [], cancelled: true },
172
- };
173
- }
174
- if (result.kind === "multiple") {
175
- return {
176
- content: [{ type: "text", text: `User selected multiple answers:\n${result.answers.map((answer) => `- ${answer}`).join("\n")}` }],
177
- details: {
178
- question: params.question, options: simpleOptions, mode: "multiple", answers: result.answers,
179
- selectedIndices: result.selectedIndices, ...(result.customAnswer === undefined ? {} : { customAnswer: result.customAnswer }),
180
- },
181
- };
182
- }
183
- if (result.kind === "cancelled") {
184
- return { content: [{ type: "text", text: "User cancelled the question" }], details: { question: params.question, options: simpleOptions, answer: null, cancelled: true } };
185
- }
186
- if (result.kind === "custom") {
187
- return { content: [{ type: "text", text: `User wrote: ${result.answer}` }], details: { question: params.question, options: simpleOptions, answer: result.answer, wasCustom: true } };
188
- }
189
- return {
190
- content: [{ type: "text", text: `User selected: ${result.answer}` }],
191
- details: { question: params.question, options: simpleOptions, answer: result.answer, selectedIndex: result.originalIndex, wasCustom: false },
192
- };
193
- },
194
-
195
- renderCall(args, theme, context) {
196
- const { mode, minSelections: minimum, maxSelections: maximum } = normalizeQuestionSelection(args);
197
- const multiple = mode === "multiple";
198
- const question = safeTerminalText(args.question);
199
- const options = args.options.map((option) => ({
200
- label: safeTerminalText(option.label),
201
- description: option.description === undefined ? undefined : safeTerminalText(option.description),
202
- preview: option.preview === undefined ? undefined : safeTerminalText(option.preview),
203
- }));
204
- if (!context.expanded) {
205
- const title = multiple ? "question (multi-select) " : "question ";
206
- const detail = multiple ? `${options.length} options · choose ${minimum}–${maximum}` : `${options.length} option${options.length === 1 ? "" : "s"}`;
207
- return new BoundedText(`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", oneLine(question))}\n${theme.fg("dim", ` ${detail}`)}`, 3);
208
- }
209
- const title = multiple ? "question (multi-select) " : "question ";
210
- const lines = [`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", question)}`];
211
- if (multiple) lines.push(theme.fg("dim", `${options.length} options · choose ${minimum}–${maximum}`));
212
- options.forEach((option, index) => {
213
- lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${index + 1}. ${option.label}`));
214
- if (option.description) lines.push(theme.fg("muted", ` ${option.description}`));
215
- if (option.preview) lines.push(theme.fg("dim", option.preview));
216
- });
217
- lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${options.length + 1}. Type a custom answer`));
218
- return new BoundedText(lines.join("\n"));
219
- },
220
-
221
- renderResult(result, options, theme) {
222
- const details = result.details;
223
- if (!details) {
224
- const first = result.content[0];
225
- return new BoundedText(first?.type === "text" ? safeTerminalText(first.text) : "", options.expanded ? undefined : 3);
226
- }
227
- if (details.cancelled || ("answer" in details && details.answer === null)) return new BoundedText(theme.fg("warning", "Cancelled"));
228
- if ("mode" in details && details.mode === "multiple") {
229
- const answers = details.answers.map(safeTerminalText);
230
- const customAnswer = details.customAnswer === undefined ? undefined : safeTerminalText(details.customAnswer);
231
- return new MultipleResultText(answers, options.expanded, customAnswer, theme.fg.bind(theme));
232
- }
233
- if (!("answer" in details) || details.answer === null) return new BoundedText("");
234
- const answer = safeTerminalText(details.answer);
235
- if (details.wasCustom) {
236
- return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", answer)}`, options.expanded ? undefined : 3);
237
- }
238
- return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("accent", answer)}`);
239
- },
240
- };
241
- pi.registerTool(questionTool);
242
- }
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import {
3
+ type ExtensionAPI,
4
+ type ToolDefinition,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { Type, type Static } from "typebox";
7
+ import { BoundedText } from "./bounded-text.ts";
8
+ import { CUSTOM_INPUT_HISTORY_BYTES, CUSTOM_INPUT_HISTORY_LIMIT, FILTER_QUERY_MAX_BYTES, FILTER_QUERY_MAX_CHARACTERS, MultipleResultText, oneLine, openQuestionUi, type DisplayOption } from "./question-ui.ts";
9
+ import { safeTerminalText } from "./safe-terminal-text.ts";
10
+
11
+ const OptionSchema = Type.Object({
12
+ label: Type.String({ minLength: 1, maxLength: 200, description: "Display label for the option" }),
13
+ description: Type.Optional(Type.String({ maxLength: 500, description: "Optional detail shown for the selected option" })),
14
+ preview: Type.Optional(Type.String({ maxLength: 8_000, description: "Optional markdown proposal preview shown for the selected option" })),
15
+ });
16
+
17
+ const QuestionParams = Type.Object({
18
+ question: Type.String({ minLength: 1, maxLength: 1_000, description: "The question to ask the user" }),
19
+ options: Type.Array(OptionSchema, {
20
+ minItems: 1,
21
+ maxItems: 9,
22
+ description: "Between 1 and 9 options for the user to choose from",
23
+ }),
24
+ mode: Type.Optional(StringEnum(["single", "multiple"] as const, {
25
+ description: "Choose one answer or multiple answers; defaults to single",
26
+ })),
27
+ minSelections: Type.Optional(Type.Integer({
28
+ minimum: 1,
29
+ maximum: 10,
30
+ description: "Minimum answers required in multiple mode; defaults to 1. Single mode accepts only an explicit 1/1 bounds pair",
31
+ })),
32
+ maxSelections: Type.Optional(Type.Integer({
33
+ minimum: 1,
34
+ maximum: 10,
35
+ description: "Maximum answers allowed in multiple mode; defaults to all options plus one custom answer. Single mode accepts only an explicit 1/1 bounds pair",
36
+ })),
37
+ });
38
+
39
+ type QuestionParamsValue = Static<typeof QuestionParams>;
40
+
41
+ type NormalizedQuestionSelection =
42
+ | { mode: "single"; minSelections: 1; maxSelections: 1 }
43
+ | { mode: "multiple"; minSelections: number; maxSelections: number };
44
+
45
+ function normalizeQuestionSelection(params: QuestionParamsValue): NormalizedQuestionSelection {
46
+ const mode = params.mode ?? "single";
47
+ if (mode === "single") {
48
+ const hasSelectionBounds = params.minSelections !== undefined || params.maxSelections !== undefined;
49
+ if (hasSelectionBounds && (params.minSelections !== 1 || params.maxSelections !== 1)) {
50
+ throw new Error("Single-select question bounds must be omitted or both be 1");
51
+ }
52
+ return { mode, minSelections: 1, maxSelections: 1 };
53
+ }
54
+
55
+ const maximumAvailable = params.options.length + 1;
56
+ const minSelections = params.minSelections ?? 1;
57
+ const maxSelections = params.maxSelections ?? maximumAvailable;
58
+ if (minSelections > maxSelections) {
59
+ throw new Error("Question minimum selections cannot exceed maximum selections");
60
+ }
61
+ if (maxSelections > maximumAvailable) {
62
+ throw new Error(`Question allows at most ${maximumAvailable} selections including one custom answer`);
63
+ }
64
+ return { mode, minSelections, maxSelections };
65
+ }
66
+
67
+ interface SingleQuestionDetails {
68
+ question: string;
69
+ options: string[];
70
+ answer: string | null;
71
+ selectedIndex?: number;
72
+ wasCustom?: boolean;
73
+ cancelled?: boolean;
74
+ }
75
+
76
+ interface MultipleQuestionDetails {
77
+ question: string;
78
+ options: string[];
79
+ mode: "multiple";
80
+ answers: string[];
81
+ selectedIndices: number[];
82
+ customAnswer?: string;
83
+ cancelled?: boolean;
84
+ }
85
+
86
+ type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
87
+
88
+ export function registerQuestionTool(pi: ExtensionAPI): void {
89
+ const customInputHistory: string[] = [];
90
+ const pendingQuestions = new Set<AbortController>();
91
+ let customInputHistoryBytes = 0;
92
+ const clearCustomInputHistory = (): void => {
93
+ for (const pending of pendingQuestions) pending.abort();
94
+ customInputHistory.length = 0;
95
+ customInputHistoryBytes = 0;
96
+ };
97
+ const rememberCustomInput = (value: string): boolean => {
98
+ const bytes = Buffer.byteLength(value, "utf8");
99
+ if (bytes > CUSTOM_INPUT_HISTORY_BYTES) return false;
100
+ const existingIndex = customInputHistory.indexOf(value);
101
+ if (existingIndex >= 0) {
102
+ const existing = customInputHistory[existingIndex];
103
+ if (existing !== undefined) customInputHistoryBytes -= Buffer.byteLength(existing, "utf8");
104
+ customInputHistory.splice(existingIndex, 1);
105
+ }
106
+ while (customInputHistory.length >= CUSTOM_INPUT_HISTORY_LIMIT || customInputHistoryBytes + bytes > CUSTOM_INPUT_HISTORY_BYTES) {
107
+ const removed = customInputHistory.shift();
108
+ if (removed !== undefined) customInputHistoryBytes -= Buffer.byteLength(removed, "utf8");
109
+ }
110
+ customInputHistory.push(value);
111
+ customInputHistoryBytes += bytes;
112
+ return true;
113
+ };
114
+ pi.on("session_start", clearCustomInputHistory);
115
+ pi.on("session_tree", clearCustomInputHistory);
116
+ pi.on("session_shutdown", clearCustomInputHistory);
117
+
118
+ const questionTool: ToolDefinition<typeof QuestionParams, QuestionDetails> = {
119
+ name: "question",
120
+ label: "Question",
121
+ description: `Ask one interactive multiple-choice question. Provide 1-9 concise options. Single-select is the default; opt into bounded multi-select with mode "multiple". The user can filter options or type a custom answer. Filter queries are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes.`,
122
+ promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
123
+ promptGuidelines: [
124
+ "Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
125
+ "Use multiple mode only when the user may need to choose more than one answer; ordinary either/or decisions remain single-select.",
126
+ ],
127
+ parameters: QuestionParams,
128
+ exposure: "model-only",
129
+ executionMode: "sequential",
130
+
131
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
132
+ const { mode, minSelections, maxSelections } = normalizeQuestionSelection(params);
133
+ if (ctx.mode !== "tui") throw new Error("The question tool requires interactive TUI mode");
134
+ if (signal?.aborted) throw new Error("Question cancelled before it opened");
135
+
136
+ const question = safeTerminalText(params.question);
137
+ const options: DisplayOption[] = params.options.map((option, index) => ({
138
+ label: safeTerminalText(option.label),
139
+ answer: option.label,
140
+ description: option.description === undefined ? undefined : safeTerminalText(option.description),
141
+ preview: option.preview === undefined ? undefined : safeTerminalText(option.preview),
142
+ originalIndex: index + 1,
143
+ }));
144
+
145
+ const pending = new AbortController();
146
+ pendingQuestions.add(pending);
147
+ let result;
148
+ try {
149
+ result = await openQuestionUi({
150
+ ctx,
151
+ signal: signal ? AbortSignal.any([signal, pending.signal]) : pending.signal,
152
+ question,
153
+ options,
154
+ mode,
155
+ minSelections,
156
+ maxSelections,
157
+ customInputHistory,
158
+ rememberCustomInput,
159
+ });
160
+ } finally {
161
+ pendingQuestions.delete(pending);
162
+ }
163
+
164
+ const simpleOptions = params.options.map((option) => option.label);
165
+ if (result.kind === "aborted" || pending.signal.aborted || signal?.aborted) {
166
+ throw new Error("Question cancelled because the agent operation was aborted");
167
+ }
168
+ if (result.kind === "cancelled" && mode === "multiple") {
169
+ return {
170
+ content: [{ type: "text", text: "User cancelled the question" }],
171
+ details: { question: params.question, options: simpleOptions, mode: "multiple", answers: [], selectedIndices: [], cancelled: true },
172
+ };
173
+ }
174
+ if (result.kind === "multiple") {
175
+ return {
176
+ content: [{ type: "text", text: `User selected multiple answers:\n${result.answers.map((answer) => `- ${answer}`).join("\n")}` }],
177
+ details: {
178
+ question: params.question, options: simpleOptions, mode: "multiple", answers: result.answers,
179
+ selectedIndices: result.selectedIndices, ...(result.customAnswer === undefined ? {} : { customAnswer: result.customAnswer }),
180
+ },
181
+ };
182
+ }
183
+ if (result.kind === "cancelled") {
184
+ return { content: [{ type: "text", text: "User cancelled the question" }], details: { question: params.question, options: simpleOptions, answer: null, cancelled: true } };
185
+ }
186
+ if (result.kind === "custom") {
187
+ return { content: [{ type: "text", text: `User wrote: ${result.answer}` }], details: { question: params.question, options: simpleOptions, answer: result.answer, wasCustom: true } };
188
+ }
189
+ return {
190
+ content: [{ type: "text", text: `User selected: ${result.answer}` }],
191
+ details: { question: params.question, options: simpleOptions, answer: result.answer, selectedIndex: result.originalIndex, wasCustom: false },
192
+ };
193
+ },
194
+
195
+ renderCall(args, theme, context) {
196
+ const { mode, minSelections: minimum, maxSelections: maximum } = normalizeQuestionSelection(args);
197
+ const multiple = mode === "multiple";
198
+ const question = safeTerminalText(args.question);
199
+ const options = args.options.map((option) => ({
200
+ label: safeTerminalText(option.label),
201
+ description: option.description === undefined ? undefined : safeTerminalText(option.description),
202
+ preview: option.preview === undefined ? undefined : safeTerminalText(option.preview),
203
+ }));
204
+ if (!context.expanded) {
205
+ const title = multiple ? "question (multi-select) " : "question ";
206
+ const detail = multiple ? `${options.length} options · choose ${minimum}–${maximum}` : `${options.length} option${options.length === 1 ? "" : "s"}`;
207
+ return new BoundedText(`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", oneLine(question))}\n${theme.fg("dim", ` ${detail}`)}`, 3);
208
+ }
209
+ const title = multiple ? "question (multi-select) " : "question ";
210
+ const lines = [`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", question)}`];
211
+ if (multiple) lines.push(theme.fg("dim", `${options.length} options · choose ${minimum}–${maximum}`));
212
+ options.forEach((option, index) => {
213
+ lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${index + 1}. ${option.label}`));
214
+ if (option.description) lines.push(theme.fg("muted", ` ${option.description}`));
215
+ if (option.preview) lines.push(theme.fg("dim", option.preview));
216
+ });
217
+ lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${options.length + 1}. Type a custom answer`));
218
+ return new BoundedText(lines.join("\n"));
219
+ },
220
+
221
+ renderResult(result, options, theme) {
222
+ const details = result.details;
223
+ if (!details) {
224
+ const first = result.content[0];
225
+ return new BoundedText(first?.type === "text" ? safeTerminalText(first.text) : "", options.expanded ? undefined : 3);
226
+ }
227
+ if (details.cancelled || ("answer" in details && details.answer === null)) return new BoundedText(theme.fg("warning", "Cancelled"));
228
+ if ("mode" in details && details.mode === "multiple") {
229
+ const answers = details.answers.map(safeTerminalText);
230
+ const customAnswer = details.customAnswer === undefined ? undefined : safeTerminalText(details.customAnswer);
231
+ return new MultipleResultText(answers, options.expanded, customAnswer, theme.fg.bind(theme));
232
+ }
233
+ if (!("answer" in details) || details.answer === null) return new BoundedText("");
234
+ const answer = safeTerminalText(details.answer);
235
+ if (details.wasCustom) {
236
+ return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", answer)}`, options.expanded ? undefined : 3);
237
+ }
238
+ return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("accent", answer)}`);
239
+ },
240
+ };
241
+ pi.registerTool(questionTool);
242
+ }