killeros 2.0.5 → 2.0.7
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/CHANGELOG.md +38 -0
- package/Killeros.ts +4 -0
- package/README.md +19 -12
- package/killeros/activity.ts +97 -0
- package/killeros/footer.ts +31 -45
- package/killeros/goals.ts +118 -42
- package/killeros/init.ts +23 -2
- package/killeros/question.ts +382 -186
- package/killeros/runtime.ts +9 -0
- package/killeros/shell-ui.ts +14 -68
- package/killeros/variants.ts +74 -32
- package/killeros/worked-for.ts +135 -0
- package/package.json +7 -6
package/killeros/question.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
2
|
import { type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
decodeKittyPrintable,
|
|
4
5
|
Editor,
|
|
5
6
|
Markdown,
|
|
6
7
|
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
7
9
|
wrapTextWithAnsi,
|
|
8
10
|
type EditorTheme,
|
|
9
11
|
} from "@earendil-works/pi-tui";
|
|
10
|
-
import { Type } from "typebox";
|
|
12
|
+
import { Type, type Static } from "typebox";
|
|
11
13
|
import { BoundedText } from "./bounded-text.ts";
|
|
12
14
|
|
|
13
15
|
const OptionSchema = Type.Object({
|
|
@@ -23,8 +25,49 @@ const QuestionParams = Type.Object({
|
|
|
23
25
|
maxItems: 9,
|
|
24
26
|
description: "Between 1 and 9 options for the user to choose from",
|
|
25
27
|
}),
|
|
28
|
+
mode: Type.Optional(StringEnum(["single", "multiple"] as const, {
|
|
29
|
+
description: "Choose one answer or multiple answers; defaults to single",
|
|
30
|
+
})),
|
|
31
|
+
minSelections: Type.Optional(Type.Integer({
|
|
32
|
+
minimum: 1,
|
|
33
|
+
maximum: 10,
|
|
34
|
+
description: "Minimum answers required in multiple mode; defaults to 1. Single mode accepts only an explicit 1/1 bounds pair",
|
|
35
|
+
})),
|
|
36
|
+
maxSelections: Type.Optional(Type.Integer({
|
|
37
|
+
minimum: 1,
|
|
38
|
+
maximum: 10,
|
|
39
|
+
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",
|
|
40
|
+
})),
|
|
26
41
|
});
|
|
27
42
|
|
|
43
|
+
type QuestionParamsValue = Static<typeof QuestionParams>;
|
|
44
|
+
|
|
45
|
+
type NormalizedQuestionSelection =
|
|
46
|
+
| { mode: "single"; minSelections: 1; maxSelections: 1 }
|
|
47
|
+
| { mode: "multiple"; minSelections: number; maxSelections: number };
|
|
48
|
+
|
|
49
|
+
function normalizeQuestionSelection(params: QuestionParamsValue): NormalizedQuestionSelection {
|
|
50
|
+
const mode = params.mode ?? "single";
|
|
51
|
+
if (mode === "single") {
|
|
52
|
+
const hasSelectionBounds = params.minSelections !== undefined || params.maxSelections !== undefined;
|
|
53
|
+
if (hasSelectionBounds && (params.minSelections !== 1 || params.maxSelections !== 1)) {
|
|
54
|
+
throw new Error("Single-select question bounds must be omitted or both be 1");
|
|
55
|
+
}
|
|
56
|
+
return { mode, minSelections: 1, maxSelections: 1 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maximumAvailable = params.options.length + 1;
|
|
60
|
+
const minSelections = params.minSelections ?? 1;
|
|
61
|
+
const maxSelections = params.maxSelections ?? maximumAvailable;
|
|
62
|
+
if (minSelections > maxSelections) {
|
|
63
|
+
throw new Error("Question minimum selections cannot exceed maximum selections");
|
|
64
|
+
}
|
|
65
|
+
if (maxSelections > maximumAvailable) {
|
|
66
|
+
throw new Error(`Question allows at most ${maximumAvailable} selections including one custom answer`);
|
|
67
|
+
}
|
|
68
|
+
return { mode, minSelections, maxSelections };
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
interface DisplayOption {
|
|
29
72
|
label: string;
|
|
30
73
|
description?: string;
|
|
@@ -33,7 +76,7 @@ interface DisplayOption {
|
|
|
33
76
|
isOther: boolean;
|
|
34
77
|
}
|
|
35
78
|
|
|
36
|
-
interface
|
|
79
|
+
interface SingleQuestionDetails {
|
|
37
80
|
question: string;
|
|
38
81
|
options: string[];
|
|
39
82
|
answer: string | null;
|
|
@@ -42,9 +85,22 @@ interface QuestionDetails {
|
|
|
42
85
|
cancelled?: boolean;
|
|
43
86
|
}
|
|
44
87
|
|
|
88
|
+
interface MultipleQuestionDetails {
|
|
89
|
+
question: string;
|
|
90
|
+
options: string[];
|
|
91
|
+
mode: "multiple";
|
|
92
|
+
answers: string[];
|
|
93
|
+
selectedIndices: number[];
|
|
94
|
+
customAnswer?: string;
|
|
95
|
+
cancelled?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
|
|
99
|
+
|
|
45
100
|
type QuestionSelection =
|
|
46
101
|
| { kind: "selected"; answer: string; originalIndex: number }
|
|
47
102
|
| { kind: "custom"; answer: string }
|
|
103
|
+
| { kind: "multiple"; answers: string[]; selectedIndices: number[]; customAnswer?: string }
|
|
48
104
|
| { kind: "cancelled" }
|
|
49
105
|
| { kind: "aborted" };
|
|
50
106
|
|
|
@@ -107,6 +163,53 @@ function boundedQuestionLines(question: string, width: number, rowLimit: number)
|
|
|
107
163
|
return visible;
|
|
108
164
|
}
|
|
109
165
|
|
|
166
|
+
function compactMultipleAnswers(answers: readonly string[], width: number): string {
|
|
167
|
+
const prefix = "✓ ";
|
|
168
|
+
if (answers.length === 0) return truncateToWidth(prefix + "No answers", width, "…");
|
|
169
|
+
const visible: string[] = [];
|
|
170
|
+
for (let index = 0; index < answers.length; index += 1) {
|
|
171
|
+
const remaining = answers.length - index - 1;
|
|
172
|
+
const candidate = [...visible, oneLine(answers[index]!)].join(", ");
|
|
173
|
+
const suffix = remaining > 0 ? `, +${remaining} more` : "";
|
|
174
|
+
if (visibleWidth(prefix + candidate + suffix) > width) break;
|
|
175
|
+
visible.push(oneLine(answers[index]!));
|
|
176
|
+
}
|
|
177
|
+
if (visible.length === answers.length) return prefix + visible.join(", ");
|
|
178
|
+
const hidden = answers.length - visible.length;
|
|
179
|
+
if (visible.length === 0) return truncateToWidth(`${prefix}+${hidden} more`, width, "…");
|
|
180
|
+
return truncateToWidth(`${prefix}${visible.join(", ")}, +${hidden} more`, width, "…");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class MultipleResultText {
|
|
184
|
+
private readonly answers: readonly string[];
|
|
185
|
+
private readonly expanded: boolean;
|
|
186
|
+
private readonly customAnswer: string | undefined;
|
|
187
|
+
private readonly color: (name: ThemeColor, text: string) => string;
|
|
188
|
+
|
|
189
|
+
constructor(
|
|
190
|
+
answers: readonly string[],
|
|
191
|
+
expanded: boolean,
|
|
192
|
+
customAnswer: string | undefined,
|
|
193
|
+
color: (name: ThemeColor, text: string) => string,
|
|
194
|
+
) {
|
|
195
|
+
this.answers = answers;
|
|
196
|
+
this.expanded = expanded;
|
|
197
|
+
this.customAnswer = customAnswer;
|
|
198
|
+
this.color = color;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
render(width: number): string[] {
|
|
202
|
+
if (width <= 0) return [];
|
|
203
|
+
if (!this.expanded) return [this.color("accent", compactMultipleAnswers(this.answers, width))];
|
|
204
|
+
return this.answers.flatMap((answer) => wrapTextWithAnsi(
|
|
205
|
+
`${this.color("success", "✓ ")}${answer === this.customAnswer ? this.color("muted", "(wrote) ") : ""}${this.color("accent", answer)}`,
|
|
206
|
+
width,
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
invalidate(): void {}
|
|
211
|
+
}
|
|
212
|
+
|
|
110
213
|
export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
111
214
|
const customInputHistory: string[] = [];
|
|
112
215
|
let customInputHistoryBytes = 0;
|
|
@@ -142,15 +245,17 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
142
245
|
pi.registerTool<typeof QuestionParams, QuestionDetails>({
|
|
143
246
|
name: "question",
|
|
144
247
|
label: "Question",
|
|
145
|
-
description: `Ask one interactive multiple-choice question. Provide 1-9 concise options. 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.`,
|
|
248
|
+
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.`,
|
|
146
249
|
promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
|
|
147
250
|
promptGuidelines: [
|
|
148
251
|
"Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
|
|
252
|
+
"Use multiple mode only when the user may need to choose more than one answer; ordinary either/or decisions remain single-select.",
|
|
149
253
|
],
|
|
150
254
|
parameters: QuestionParams,
|
|
151
255
|
executionMode: "sequential",
|
|
152
256
|
|
|
153
257
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
258
|
+
const { mode, minSelections, maxSelections } = normalizeQuestionSelection(params);
|
|
154
259
|
if (ctx.mode !== "tui") throw new Error("The question tool requires interactive TUI mode");
|
|
155
260
|
if (signal?.aborted) throw new Error("Question cancelled before it opened");
|
|
156
261
|
|
|
@@ -162,18 +267,17 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
162
267
|
originalIndex: index + 1,
|
|
163
268
|
isOther: false,
|
|
164
269
|
})),
|
|
165
|
-
{
|
|
166
|
-
label: "Type a custom answer",
|
|
167
|
-
originalIndex: params.options.length + 1,
|
|
168
|
-
isOther: true,
|
|
169
|
-
},
|
|
270
|
+
{ label: "Type a custom answer", originalIndex: params.options.length + 1, isOther: true },
|
|
170
271
|
];
|
|
171
272
|
|
|
172
273
|
let finishFromAbort: (() => void) | undefined;
|
|
173
274
|
const resultPromise = ctx.ui.custom<QuestionSelection>((tui, theme, keybindings, done) => {
|
|
174
275
|
let optionIndex = 0;
|
|
175
|
-
|
|
276
|
+
type EditMode = "none" | "filter" | "custom";
|
|
277
|
+
let editMode: EditMode = "none";
|
|
176
278
|
let filterQuery = "";
|
|
279
|
+
const selectedOriginalIndices = new Set<number>();
|
|
280
|
+
let customAnswer: string | undefined;
|
|
177
281
|
let cachedWidth: number | undefined;
|
|
178
282
|
let cachedRows: number | undefined;
|
|
179
283
|
let cachedLines: string[] | undefined;
|
|
@@ -186,17 +290,11 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
186
290
|
};
|
|
187
291
|
finishFromAbort = () => finish({ kind: "aborted" });
|
|
188
292
|
|
|
189
|
-
const keyHint = (
|
|
190
|
-
keybinding: Parameters<typeof keybindings.getKeys>[0],
|
|
191
|
-
description: string,
|
|
192
|
-
): string => {
|
|
293
|
+
const keyHint = (keybinding: Parameters<typeof keybindings.getKeys>[0], description: string): string => {
|
|
193
294
|
const keyText = keybindings.getKeys(keybinding)
|
|
194
295
|
.join("/")
|
|
195
296
|
.split("/")
|
|
196
|
-
.map((key) => key
|
|
197
|
-
.split("+")
|
|
198
|
-
.map((part) => process.platform === "darwin" && part.toLocaleLowerCase() === "alt" ? "option" : part)
|
|
199
|
-
.join("+"))
|
|
297
|
+
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLocaleLowerCase() === "alt" ? "option" : part).join("+"))
|
|
200
298
|
.join("/");
|
|
201
299
|
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
202
300
|
};
|
|
@@ -221,6 +319,16 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
221
319
|
|| option.label.toLocaleLowerCase().includes(query)
|
|
222
320
|
|| option.description?.toLocaleLowerCase().includes(query));
|
|
223
321
|
};
|
|
322
|
+
const selectedCount = (): number => selectedOriginalIndices.size + (customAnswer === undefined ? 0 : 1);
|
|
323
|
+
const orderedMultipleSelection = () => {
|
|
324
|
+
const selectedIndices = [...selectedOriginalIndices].sort((left, right) => left - right);
|
|
325
|
+
const predefined = selectedIndices.map((index) => params.options[index - 1]!.label);
|
|
326
|
+
return {
|
|
327
|
+
answers: customAnswer === undefined ? predefined : [...predefined, customAnswer],
|
|
328
|
+
selectedIndices,
|
|
329
|
+
...(customAnswer === undefined ? {} : { customAnswer }),
|
|
330
|
+
};
|
|
331
|
+
};
|
|
224
332
|
|
|
225
333
|
const invalidate = (): void => {
|
|
226
334
|
cachedWidth = undefined;
|
|
@@ -228,34 +336,83 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
228
336
|
cachedLines = undefined;
|
|
229
337
|
editor.invalidate();
|
|
230
338
|
};
|
|
231
|
-
|
|
232
339
|
const refresh = (): void => {
|
|
233
340
|
invalidate();
|
|
234
341
|
tui.requestRender();
|
|
235
342
|
};
|
|
236
|
-
|
|
343
|
+
const notifyFilterLimit = (): void => ctx.ui.notify(
|
|
344
|
+
`Question filters are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes`,
|
|
345
|
+
"error",
|
|
346
|
+
);
|
|
237
347
|
const appendFilterInput = (value: string): void => {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
ctx.ui.notify(
|
|
242
|
-
`Question filters are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes`,
|
|
243
|
-
"error",
|
|
244
|
-
);
|
|
348
|
+
const next = filterQuery + value;
|
|
349
|
+
if (inputCharacterCount(next) > FILTER_QUERY_MAX_CHARACTERS || Buffer.byteLength(next, "utf8") > FILTER_QUERY_MAX_BYTES) {
|
|
350
|
+
notifyFilterLimit();
|
|
245
351
|
return;
|
|
246
352
|
}
|
|
247
|
-
filterQuery
|
|
353
|
+
filterQuery = next;
|
|
248
354
|
optionIndex = 0;
|
|
249
355
|
refresh();
|
|
250
356
|
};
|
|
357
|
+
const togglePredefined = (option: DisplayOption): void => {
|
|
358
|
+
if (selectedOriginalIndices.has(option.originalIndex)) {
|
|
359
|
+
selectedOriginalIndices.delete(option.originalIndex);
|
|
360
|
+
refresh();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (selectedCount() >= maxSelections) {
|
|
364
|
+
ctx.ui.notify(`Select at most ${maxSelections} answer${maxSelections === 1 ? "" : "s"}`, "error");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
selectedOriginalIndices.add(option.originalIndex);
|
|
368
|
+
refresh();
|
|
369
|
+
};
|
|
370
|
+
const enterCustomMode = (): void => {
|
|
371
|
+
editMode = "custom";
|
|
372
|
+
editor.setText(mode === "multiple" ? customAnswer ?? "" : "");
|
|
373
|
+
refresh();
|
|
374
|
+
};
|
|
375
|
+
const enterFilterMode = (): void => {
|
|
376
|
+
editMode = "filter";
|
|
377
|
+
editor.setText(filterQuery);
|
|
378
|
+
refresh();
|
|
379
|
+
};
|
|
251
380
|
|
|
252
381
|
editor.onSubmit = (value) => {
|
|
382
|
+
if (editMode === "filter") {
|
|
383
|
+
filterQuery = value;
|
|
384
|
+
optionIndex = 0;
|
|
385
|
+
editMode = "none";
|
|
386
|
+
editor.setText("");
|
|
387
|
+
refresh();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
253
390
|
const answer = value.trim();
|
|
254
391
|
if (answer) {
|
|
255
392
|
if (inputCharacterCount(answer) > CUSTOM_INPUT_MAX_CHARACTERS) {
|
|
256
393
|
ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
|
|
257
394
|
return;
|
|
258
395
|
}
|
|
396
|
+
if (mode === "multiple") {
|
|
397
|
+
const addsSelection = customAnswer === undefined;
|
|
398
|
+
if (addsSelection && selectedCount() >= maxSelections) {
|
|
399
|
+
editor.setText(value);
|
|
400
|
+
ctx.ui.notify(`Select at most ${maxSelections} answer${maxSelections === 1 ? "" : "s"}`, "error");
|
|
401
|
+
refresh();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!rememberCustomInput(answer)) {
|
|
405
|
+
editor.setText(value);
|
|
406
|
+
ctx.ui.notify(`Custom answer history is limited to ${CUSTOM_INPUT_HISTORY_BYTES} bytes`, "error");
|
|
407
|
+
refresh();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
customAnswer = answer;
|
|
411
|
+
editMode = "none";
|
|
412
|
+
editor.setText("");
|
|
413
|
+
refresh();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
259
416
|
if (!rememberCustomInput(answer)) {
|
|
260
417
|
ctx.ui.notify(`Custom answer history is limited to ${CUSTOM_INPUT_HISTORY_BYTES} bytes`, "error");
|
|
261
418
|
return;
|
|
@@ -263,32 +420,35 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
263
420
|
finish({ kind: "custom", answer });
|
|
264
421
|
return;
|
|
265
422
|
}
|
|
266
|
-
editMode =
|
|
423
|
+
editMode = "none";
|
|
267
424
|
editor.setText("");
|
|
268
425
|
refresh();
|
|
269
426
|
};
|
|
270
427
|
|
|
271
|
-
const
|
|
272
|
-
|
|
428
|
+
const handleEditorInput = (data: string): void => {
|
|
429
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
430
|
+
editMode = "none";
|
|
431
|
+
editor.setText("");
|
|
432
|
+
refresh();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const before = editor.getExpandedText();
|
|
436
|
+
editor.handleInput(data);
|
|
437
|
+
const after = editor.getExpandedText();
|
|
438
|
+
const overLimit = editMode === "filter"
|
|
439
|
+
? inputCharacterCount(after) > FILTER_QUERY_MAX_CHARACTERS || Buffer.byteLength(after, "utf8") > FILTER_QUERY_MAX_BYTES
|
|
440
|
+
: inputCharacterCount(after) > CUSTOM_INPUT_MAX_CHARACTERS;
|
|
441
|
+
if (overLimit) {
|
|
442
|
+
editor.setText(before);
|
|
443
|
+
if (editMode === "filter") notifyFilterLimit();
|
|
444
|
+
else ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
|
|
445
|
+
}
|
|
273
446
|
refresh();
|
|
274
447
|
};
|
|
275
448
|
|
|
276
449
|
const handleInput = (data: string): void => {
|
|
277
|
-
if (editMode) {
|
|
278
|
-
|
|
279
|
-
editMode = false;
|
|
280
|
-
editor.setText("");
|
|
281
|
-
refresh();
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
const before = editor.getExpandedText();
|
|
285
|
-
editor.handleInput(data);
|
|
286
|
-
const after = editor.getExpandedText();
|
|
287
|
-
if (inputCharacterCount(after) > CUSTOM_INPUT_MAX_CHARACTERS) {
|
|
288
|
-
editor.setText(before);
|
|
289
|
-
ctx.ui.notify(`Custom answers are limited to ${CUSTOM_INPUT_MAX_CHARACTERS} characters`, "error");
|
|
290
|
-
}
|
|
291
|
-
refresh();
|
|
450
|
+
if (editMode !== "none") {
|
|
451
|
+
handleEditorInput(data);
|
|
292
452
|
return;
|
|
293
453
|
}
|
|
294
454
|
|
|
@@ -315,6 +475,53 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
315
475
|
refresh();
|
|
316
476
|
return;
|
|
317
477
|
}
|
|
478
|
+
|
|
479
|
+
const printableInput = decodeQuestionFilterInput(data);
|
|
480
|
+
const isPasteInput = data.includes("\x1B[200~");
|
|
481
|
+
if (mode === "multiple") {
|
|
482
|
+
const selected = visibleOptions[optionIndex];
|
|
483
|
+
if (!isPasteInput && printableInput === " ") {
|
|
484
|
+
if (!selected) return;
|
|
485
|
+
if (selected.isOther) {
|
|
486
|
+
if (customAnswer !== undefined) {
|
|
487
|
+
customAnswer = undefined;
|
|
488
|
+
refresh();
|
|
489
|
+
}
|
|
490
|
+
} else togglePredefined(selected);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!isPasteInput && printableInput === "/") {
|
|
494
|
+
enterFilterMode();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!isPasteInput && printableInput && /^[1-9]$/u.test(printableInput)) {
|
|
498
|
+
const numbered = visibleOptions[Number(printableInput) - 1];
|
|
499
|
+
if (!numbered) return;
|
|
500
|
+
if (numbered.isOther) enterCustomMode();
|
|
501
|
+
else togglePredefined(numbered);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
505
|
+
if (selected?.isOther) {
|
|
506
|
+
enterCustomMode();
|
|
507
|
+
} else if (selectedCount() < minSelections) {
|
|
508
|
+
ctx.ui.notify(`Select at least ${minSelections} answer${minSelections === 1 ? "" : "s"}`, "error");
|
|
509
|
+
} else {
|
|
510
|
+
finish({ kind: "multiple", ...orderedMultipleSelection() });
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
515
|
+
if (filterQuery) {
|
|
516
|
+
filterQuery = "";
|
|
517
|
+
optionIndex = 0;
|
|
518
|
+
refresh();
|
|
519
|
+
} else finish({ kind: "cancelled" });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
318
525
|
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
319
526
|
const selected = visibleOptions[optionIndex];
|
|
320
527
|
if (!selected) return;
|
|
@@ -327,9 +534,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
327
534
|
filterQuery = "";
|
|
328
535
|
optionIndex = 0;
|
|
329
536
|
refresh();
|
|
330
|
-
} else {
|
|
331
|
-
finish({ kind: "cancelled" });
|
|
332
|
-
}
|
|
537
|
+
} else finish({ kind: "cancelled" });
|
|
333
538
|
return;
|
|
334
539
|
}
|
|
335
540
|
if (keybindings.matches(data, "tui.editor.deleteCharBackward")) {
|
|
@@ -340,9 +545,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
340
545
|
}
|
|
341
546
|
return;
|
|
342
547
|
}
|
|
343
|
-
|
|
344
|
-
const isPasteInput = data.includes("\x1B[200~");
|
|
345
|
-
if (!isPasteInput && printableInput && /^[1-9]$/.test(printableInput)) {
|
|
548
|
+
if (!isPasteInput && printableInput && /^[1-9]$/u.test(printableInput)) {
|
|
346
549
|
const selected = visibleOptions[Number(printableInput) - 1];
|
|
347
550
|
if (!selected) return;
|
|
348
551
|
if (selected.isOther) enterCustomMode();
|
|
@@ -354,137 +557,119 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
354
557
|
|
|
355
558
|
const render = (width: number): string[] => {
|
|
356
559
|
if (width <= 0) return [];
|
|
357
|
-
const renderWidth = width;
|
|
358
560
|
const rowBudget = Math.max(1, tui.terminal.rows);
|
|
359
|
-
if (cachedLines && cachedWidth ===
|
|
561
|
+
if (cachedLines && cachedWidth === width && cachedRows === rowBudget) return cachedLines;
|
|
360
562
|
const visibleOptions = filteredOptions();
|
|
361
563
|
if (optionIndex >= visibleOptions.length) optionIndex = Math.max(0, visibleOptions.length - 1);
|
|
362
564
|
const selected = visibleOptions[optionIndex];
|
|
363
565
|
const position = `Option ${Math.min(optionIndex + 1, visibleOptions.length)}/${visibleOptions.length}`;
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
const filterCount = inputCharacterCount(filterQuery).toLocaleString();
|
|
367
|
-
const compactDraft =
|
|
368
|
-
|
|
566
|
+
const editorText = editor.getExpandedText();
|
|
567
|
+
const editorCount = inputCharacterCount(editorText).toLocaleString();
|
|
568
|
+
const filterCount = inputCharacterCount(editMode === "filter" ? editorText : filterQuery).toLocaleString();
|
|
569
|
+
const compactDraft = editorText.replace(/\r\n|\r|\n/gu, " ↵ ") || (editMode === "filter" ? "Type a filter" : "Type an answer");
|
|
570
|
+
const selectionRange = minSelections === maxSelections ? `${minSelections}` : `${minSelections}–${maxSelections}`;
|
|
571
|
+
const selectionStatus = `Selected ${selectedCount()} · required ${selectionRange}`;
|
|
572
|
+
const optionLabel = (option: DisplayOption, index: number): string => {
|
|
573
|
+
if (mode === "single") return `${index === optionIndex ? ">" : " "} ${index + 1}. ${option.label}`;
|
|
574
|
+
const checked = option.isOther ? customAnswer !== undefined : selectedOriginalIndices.has(option.originalIndex);
|
|
575
|
+
const label = option.isOther && customAnswer !== undefined ? `Custom: ${oneLine(customAnswer)}` : option.label;
|
|
576
|
+
return `${index === optionIndex ? ">" : " "} ${checked ? "[x]" : "[ ]"} ${index + 1}. ${label}`;
|
|
577
|
+
};
|
|
578
|
+
const browseHint = mode === "multiple"
|
|
579
|
+
? `space toggle • / filter • ${keyHint("tui.select.confirm", selected?.isOther ? (customAnswer ? "edit custom" : "add custom") : "submit")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`
|
|
580
|
+
: `${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`;
|
|
581
|
+
const editHint = `${keyHint("tui.input.submit", editMode === "filter" ? "apply" : "submit")} • ${keyHint("tui.select.cancel", "options")}`;
|
|
582
|
+
|
|
583
|
+
let lines: string[];
|
|
369
584
|
if (rowBudget <= 2) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}`
|
|
389
|
-
: position,
|
|
390
|
-
editMode
|
|
391
|
-
? `${keyHint("tui.input.submit", "submit")} • ${keyHint("tui.select.cancel", "options")}`
|
|
392
|
-
: `${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`,
|
|
585
|
+
if (editMode !== "none") lines = [`${editMode === "filter" ? "Filter" : "Answer"} ${editMode === "filter" ? filterCount : editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`, compactDraft];
|
|
586
|
+
else if (mode === "multiple") {
|
|
587
|
+
const focusedRow = selected ? optionLabel(selected, optionIndex) : "";
|
|
588
|
+
const compactSelectionStatus = visibleWidth(selectionStatus) <= width
|
|
589
|
+
? selectionStatus
|
|
590
|
+
: `Selected ${selectedCount()}`;
|
|
591
|
+
lines = focusedRow && visibleWidth(focusedRow) <= width
|
|
592
|
+
? [focusedRow, compactSelectionStatus]
|
|
593
|
+
: [compactSelectionStatus, focusedRow];
|
|
594
|
+
} else lines = [`${selected ? `> ${selected.label}` : "No matching options"} · ${position}`];
|
|
595
|
+
} else if (rowBudget <= 5) {
|
|
596
|
+
lines = [
|
|
597
|
+
...boundedQuestionLines(params.question, width, Math.max(1, rowBudget - 3)),
|
|
598
|
+
editMode !== "none"
|
|
599
|
+
? `${editMode === "filter" ? "Filter" : "Answer"} ${editMode === "filter" ? filterCount : editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
|
|
600
|
+
: selected ? optionLabel(selected, optionIndex) : "No matching options",
|
|
601
|
+
editMode !== "none" ? compactDraft : mode === "multiple" ? selectionStatus : filterQuery ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}` : position,
|
|
602
|
+
editMode !== "none" ? editHint : browseHint,
|
|
393
603
|
];
|
|
394
|
-
cachedWidth = renderWidth;
|
|
395
|
-
cachedRows = rowBudget;
|
|
396
|
-
cachedLines = compact.slice(0, rowBudget).map((line) => boundedRenderLine(line, renderWidth, "…"));
|
|
397
|
-
return cachedLines;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const questionLines = boundedQuestionLines(params.question, renderWidth, Math.max(1, rowBudget - 5));
|
|
401
|
-
const contentRows = rowBudget - questionLines.length - 4;
|
|
402
|
-
const optionCapacity = Math.max(1, Math.min(5, Math.ceil(contentRows / 2)));
|
|
403
|
-
const detailCapacity = Math.max(0, contentRows - optionCapacity);
|
|
404
|
-
const { start, end } = visibleOptionRange(visibleOptions.length, optionIndex, optionCapacity);
|
|
405
|
-
const hiddenAbove = start > 0 ? `↑ ${start}` : "";
|
|
406
|
-
const hiddenBelowCount = visibleOptions.length - end;
|
|
407
|
-
const hiddenBelow = hiddenBelowCount > 0 ? `↓ ${hiddenBelowCount}` : "";
|
|
408
|
-
const hiddenStatus = [hiddenAbove, hiddenBelow].filter(Boolean).join(" · ");
|
|
409
|
-
const progress = editMode
|
|
410
|
-
? `Answer ${answerCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
|
|
411
|
-
: filterQuery
|
|
412
|
-
? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} · ${position}`
|
|
413
|
-
: `${position}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`;
|
|
414
|
-
const hint = editMode
|
|
415
|
-
? `${keyHint("tui.input.submit", "submit")} • ${keyHint("tui.select.cancel", "options")}`
|
|
416
|
-
: `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`;
|
|
417
|
-
const lines: string[] = [
|
|
418
|
-
theme.fg("accent", "─".repeat(renderWidth)),
|
|
419
|
-
...questionLines.map((line) => theme.fg("text", line)),
|
|
420
|
-
theme.fg("muted", truncateToWidth(` ${progress}`, renderWidth, "…")),
|
|
421
|
-
];
|
|
422
|
-
|
|
423
|
-
if (editMode) {
|
|
424
|
-
const editorLines = editor.render(renderWidth);
|
|
425
|
-
const draftLines = editorLines.length > 2 ? editorLines.slice(1, -1) : editorLines;
|
|
426
|
-
lines.push(...(draftLines.length > 0 ? draftLines : ["Type an answer"]).slice(-contentRows));
|
|
427
604
|
} else {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
italic: (text) => theme.italic(text),
|
|
457
|
-
strikethrough: (text) => theme.strikethrough(text),
|
|
458
|
-
underline: (text) => theme.underline(text),
|
|
459
|
-
},
|
|
460
|
-
{ color: (text) => theme.fg("muted", text) },
|
|
461
|
-
).render(renderWidth));
|
|
462
|
-
}
|
|
463
|
-
if (detailCapacity > 0 && detailLines.length > detailCapacity) {
|
|
464
|
-
const visibleDetailRows = Math.max(0, detailCapacity - 1);
|
|
465
|
-
lines.push(...detailLines.slice(0, visibleDetailRows));
|
|
466
|
-
const hiddenRows = detailLines.length - visibleDetailRows;
|
|
467
|
-
lines.push(theme.fg("dim", `… ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
|
|
605
|
+
const questionLines = boundedQuestionLines(params.question, width, Math.max(1, rowBudget - 5));
|
|
606
|
+
const contentRows = rowBudget - questionLines.length - 4;
|
|
607
|
+
const optionCapacity = Math.max(1, Math.min(5, Math.ceil(contentRows / 2)));
|
|
608
|
+
const detailCapacity = Math.max(0, contentRows - optionCapacity);
|
|
609
|
+
const { start, end } = visibleOptionRange(visibleOptions.length, optionIndex, optionCapacity);
|
|
610
|
+
const hiddenAbove = start > 0 ? `↑ ${start}` : "";
|
|
611
|
+
const hiddenBelowCount = visibleOptions.length - end;
|
|
612
|
+
const hiddenBelow = hiddenBelowCount > 0 ? `↓ ${hiddenBelowCount}` : "";
|
|
613
|
+
const hiddenStatus = [hiddenAbove, hiddenBelow].filter(Boolean).join(" · ");
|
|
614
|
+
const progress = editMode === "filter"
|
|
615
|
+
? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()}`
|
|
616
|
+
: editMode === "custom"
|
|
617
|
+
? `Answer ${editorCount}/${CUSTOM_INPUT_MAX_CHARACTERS.toLocaleString()}`
|
|
618
|
+
: mode === "multiple"
|
|
619
|
+
? `${selectionStatus}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`
|
|
620
|
+
: filterQuery ? `Filter ${filterCount}/${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} · ${position}` : `${position}${hiddenStatus ? ` · ${hiddenStatus}` : ""}`;
|
|
621
|
+
const navigationHint = editMode !== "none" ? editHint : mode === "multiple"
|
|
622
|
+
? `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${browseHint}`
|
|
623
|
+
: `${keyHint("tui.select.up", "up")} • ${keyHint("tui.select.down", "down")} • ${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", filterQuery ? "clear filter" : "cancel")}`;
|
|
624
|
+
lines = [
|
|
625
|
+
theme.fg("accent", "─".repeat(width)),
|
|
626
|
+
...questionLines.map((line) => theme.fg("text", line)),
|
|
627
|
+
theme.fg("muted", truncateToWidth(` ${progress}`, width, "…")),
|
|
628
|
+
];
|
|
629
|
+
if (editMode !== "none") {
|
|
630
|
+
const editorLines = editor.render(width);
|
|
631
|
+
const draftLines = editorLines.length > 2 ? editorLines.slice(1, -1) : editorLines;
|
|
632
|
+
lines.push(...(draftLines.length > 0 ? draftLines : [editMode === "filter" ? "Type a filter" : "Type an answer"]).slice(-contentRows));
|
|
468
633
|
} else {
|
|
469
|
-
|
|
634
|
+
for (let index = start; index < end; index += 1) {
|
|
635
|
+
const option = visibleOptions[index]!;
|
|
636
|
+
const color: ThemeColor = index === optionIndex ? "accent" : "text";
|
|
637
|
+
lines.push(theme.fg(color, truncateToWidth(optionLabel(option, index), width, "…")));
|
|
638
|
+
}
|
|
639
|
+
const detailLines: string[] = [];
|
|
640
|
+
if (selected?.description) detailLines.push(...wrapTextWithAnsi(theme.fg("muted", selected.description), width));
|
|
641
|
+
if (selected?.preview) {
|
|
642
|
+
detailLines.push(theme.fg("accent", theme.bold("Proposal preview")));
|
|
643
|
+
detailLines.push(...new Markdown(selected.preview, 0, 0, {
|
|
644
|
+
heading: (text) => theme.fg("accent", theme.bold(text)), link: (text) => theme.fg("accent", text),
|
|
645
|
+
linkUrl: (text) => theme.fg("dim", text), code: (text) => theme.fg("mdCode", text),
|
|
646
|
+
codeBlock: (text) => theme.fg("mdCodeBlock", text), codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
|
|
647
|
+
quote: (text) => theme.fg("mdQuote", text), quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
|
|
648
|
+
hr: (text) => theme.fg("mdHr", text), listBullet: (text) => theme.fg("mdListBullet", text),
|
|
649
|
+
bold: (text) => theme.bold(text), italic: (text) => theme.italic(text), strikethrough: (text) => theme.strikethrough(text),
|
|
650
|
+
underline: (text) => theme.underline(text),
|
|
651
|
+
}, { color: (text) => theme.fg("muted", text) }).render(width));
|
|
652
|
+
}
|
|
653
|
+
if (detailCapacity > 0 && detailLines.length > detailCapacity) {
|
|
654
|
+
const visibleDetailRows = Math.max(0, detailCapacity - 1);
|
|
655
|
+
lines.push(...detailLines.slice(0, visibleDetailRows));
|
|
656
|
+
const hiddenRows = detailLines.length - visibleDetailRows;
|
|
657
|
+
lines.push(theme.fg("dim", `… ${hiddenRows} more line${hiddenRows === 1 ? "" : "s"}`));
|
|
658
|
+
} else lines.push(...detailLines.slice(0, detailCapacity));
|
|
470
659
|
}
|
|
660
|
+
lines.push(theme.fg("dim", truncateToWidth(` ${navigationHint}`, width, "…")));
|
|
661
|
+
lines.push(theme.fg("accent", "─".repeat(width)));
|
|
471
662
|
}
|
|
472
|
-
|
|
473
|
-
lines.push(theme.fg("dim", truncateToWidth(` ${hint}`, renderWidth, "…")));
|
|
474
|
-
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
475
|
-
cachedWidth = renderWidth;
|
|
663
|
+
cachedWidth = width;
|
|
476
664
|
cachedRows = rowBudget;
|
|
477
|
-
cachedLines = lines.slice(0, rowBudget).map((line) => boundedRenderLine(line,
|
|
665
|
+
cachedLines = lines.slice(0, rowBudget).map((line) => boundedRenderLine(line, width, rowBudget <= 5 ? "…" : ""));
|
|
478
666
|
return cachedLines;
|
|
479
667
|
};
|
|
480
668
|
|
|
481
669
|
let focused = false;
|
|
482
670
|
return {
|
|
483
671
|
get focused(): boolean { return focused; },
|
|
484
|
-
set focused(value: boolean) {
|
|
485
|
-
focused = value;
|
|
486
|
-
editor.focused = value;
|
|
487
|
-
},
|
|
672
|
+
set focused(value: boolean) { focused = value; editor.focused = value; },
|
|
488
673
|
render,
|
|
489
674
|
handleInput,
|
|
490
675
|
invalidate,
|
|
@@ -503,43 +688,50 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
503
688
|
|
|
504
689
|
const simpleOptions = params.options.map((option) => option.label);
|
|
505
690
|
if (result.kind === "aborted") throw new Error("Question cancelled because the agent operation was aborted");
|
|
506
|
-
if (result.kind === "cancelled") {
|
|
691
|
+
if (result.kind === "cancelled" && mode === "multiple") {
|
|
507
692
|
return {
|
|
508
693
|
content: [{ type: "text", text: "User cancelled the question" }],
|
|
509
|
-
details: { question: params.question, options: simpleOptions,
|
|
694
|
+
details: { question: params.question, options: simpleOptions, mode: "multiple", answers: [], selectedIndices: [], cancelled: true },
|
|
510
695
|
};
|
|
511
696
|
}
|
|
512
|
-
if (result.kind === "
|
|
697
|
+
if (result.kind === "multiple") {
|
|
513
698
|
return {
|
|
514
|
-
content: [{ type: "text", text: `User
|
|
515
|
-
details: {
|
|
699
|
+
content: [{ type: "text", text: `User selected multiple answers:\n${result.answers.map((answer) => `- ${answer}`).join("\n")}` }],
|
|
700
|
+
details: {
|
|
701
|
+
question: params.question, options: simpleOptions, mode: "multiple", answers: result.answers,
|
|
702
|
+
selectedIndices: result.selectedIndices, ...(result.customAnswer === undefined ? {} : { customAnswer: result.customAnswer }),
|
|
703
|
+
},
|
|
516
704
|
};
|
|
517
705
|
}
|
|
706
|
+
if (result.kind === "cancelled") {
|
|
707
|
+
return { content: [{ type: "text", text: "User cancelled the question" }], details: { question: params.question, options: simpleOptions, answer: null, cancelled: true } };
|
|
708
|
+
}
|
|
709
|
+
if (result.kind === "custom") {
|
|
710
|
+
return { content: [{ type: "text", text: `User wrote: ${result.answer}` }], details: { question: params.question, options: simpleOptions, answer: result.answer, wasCustom: true } };
|
|
711
|
+
}
|
|
518
712
|
return {
|
|
519
713
|
content: [{ type: "text", text: `User selected: ${result.answer}` }],
|
|
520
|
-
details: {
|
|
521
|
-
question: params.question,
|
|
522
|
-
options: simpleOptions,
|
|
523
|
-
answer: result.answer,
|
|
524
|
-
selectedIndex: result.originalIndex,
|
|
525
|
-
wasCustom: false,
|
|
526
|
-
},
|
|
714
|
+
details: { question: params.question, options: simpleOptions, answer: result.answer, selectedIndex: result.originalIndex, wasCustom: false },
|
|
527
715
|
};
|
|
528
716
|
},
|
|
529
717
|
|
|
530
718
|
renderCall(args, theme, context) {
|
|
719
|
+
const { mode, minSelections: minimum, maxSelections: maximum } = normalizeQuestionSelection(args);
|
|
720
|
+
const multiple = mode === "multiple";
|
|
531
721
|
if (!context.expanded) {
|
|
532
|
-
const
|
|
533
|
-
|
|
722
|
+
const title = multiple ? "question (multi-select) " : "question ";
|
|
723
|
+
const detail = multiple ? `${args.options.length} options · choose ${minimum}–${maximum}` : `${args.options.length} option${args.options.length === 1 ? "" : "s"}`;
|
|
724
|
+
return new BoundedText(`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", oneLine(args.question))}\n${theme.fg("dim", ` ${detail}`)}`, 3);
|
|
534
725
|
}
|
|
535
|
-
|
|
536
|
-
const lines = [`${theme.fg("toolTitle", theme.bold(
|
|
726
|
+
const title = multiple ? "question (multi-select) " : "question ";
|
|
727
|
+
const lines = [`${theme.fg("toolTitle", theme.bold(title))}${theme.fg("muted", args.question)}`];
|
|
728
|
+
if (multiple) lines.push(theme.fg("dim", `${args.options.length} options · choose ${minimum}–${maximum}`));
|
|
537
729
|
args.options.forEach((option, index) => {
|
|
538
|
-
lines.push(theme.fg("text", `${index + 1}. ${option.label}`));
|
|
730
|
+
lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${index + 1}. ${option.label}`));
|
|
539
731
|
if (option.description) lines.push(theme.fg("muted", ` ${option.description}`));
|
|
540
732
|
if (option.preview) lines.push(theme.fg("dim", option.preview));
|
|
541
733
|
});
|
|
542
|
-
lines.push(theme.fg("text", `${args.options.length + 1}. Type a custom answer`));
|
|
734
|
+
lines.push(theme.fg("text", `${multiple ? "[ ] " : ""}${args.options.length + 1}. Type a custom answer`));
|
|
543
735
|
return new BoundedText(lines.join("\n"));
|
|
544
736
|
},
|
|
545
737
|
|
|
@@ -549,12 +741,16 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
549
741
|
const first = result.content[0];
|
|
550
742
|
return new BoundedText(first?.type === "text" ? first.text : "", options.expanded ? undefined : 3);
|
|
551
743
|
}
|
|
552
|
-
if (details.cancelled || details.answer === null) return new BoundedText(theme.fg("warning", "Cancelled"));
|
|
744
|
+
if (details.cancelled || ("answer" in details && details.answer === null)) return new BoundedText(theme.fg("warning", "Cancelled"));
|
|
745
|
+
if ("mode" in details && details.mode === "multiple") {
|
|
746
|
+
return new MultipleResultText(details.answers, options.expanded, details.customAnswer, theme.fg.bind(theme));
|
|
747
|
+
}
|
|
748
|
+
if (!("answer" in details) || details.answer === null) return new BoundedText("");
|
|
749
|
+
const answer = details.answer;
|
|
553
750
|
if (details.wasCustom) {
|
|
554
|
-
|
|
555
|
-
return new BoundedText(text, options.expanded ? undefined : 3);
|
|
751
|
+
return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("muted", "(wrote) ")}${theme.fg("accent", answer)}`, options.expanded ? undefined : 3);
|
|
556
752
|
}
|
|
557
|
-
return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("accent",
|
|
753
|
+
return new BoundedText(`${theme.fg("success", "✓ ")}${theme.fg("accent", answer)}`);
|
|
558
754
|
},
|
|
559
755
|
});
|
|
560
756
|
}
|