@vitos-pizza/vitos-pizza 0.4.0 → 0.4.1
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 +27 -11
- package/README.md +1 -1
- package/package.json +12 -12
- package/packages/agent-mode/package.json +3 -3
- package/packages/agent-mode/src/plan-instructions.ts +14 -7
- package/packages/hypa/package.json +1 -1
- package/packages/keybindings/package.json +1 -1
- package/packages/permission-system/package.json +2 -2
- package/packages/question/extensions/index.ts +54 -21
- package/packages/question/package.json +1 -1
- package/packages/question/src/forwarding/file-watcher.ts +41 -11
- package/packages/question/src/forwarding/forwarder.ts +61 -27
- package/packages/question/src/question-ui.ts +773 -78
- package/packages/question/src/register-tool.ts +351 -40
- package/packages/question/src/types.ts +79 -7
- package/packages/session-title/package.json +3 -3
- package/packages/settings-preset/package.json +1 -1
- package/packages/subagents/agents/planner.md +3 -3
- package/packages/subagents/agents/scout.md +2 -2
- package/packages/subagents/agents/worker.md +2 -2
- package/packages/subagents/package.json +1 -1
- package/packages/todoist/package.json +1 -1
- package/packages/ui-enhancements/package.json +1 -1
- package/packages/ui-enhancements/src/chrome/resize-recovery.ts +71 -14
- package/packages/websearch/package.json +1 -1
|
@@ -8,18 +8,51 @@ import {
|
|
|
8
8
|
visibleWidth,
|
|
9
9
|
wrapTextWithAnsi,
|
|
10
10
|
} from "@earendil-works/pi-tui";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
MultiQuestionParams,
|
|
13
|
+
MultiQuestionUiResult,
|
|
14
|
+
MultiTabAnswer,
|
|
15
|
+
QuestionParams,
|
|
16
|
+
QuestionUiResult,
|
|
17
|
+
SelectType,
|
|
18
|
+
SingleTabAnswer,
|
|
19
|
+
TabAnswer,
|
|
20
|
+
} from "./types.ts";
|
|
12
21
|
|
|
13
22
|
const CUSTOM_INPUT_PLACEHOLDER = "Type your answer";
|
|
23
|
+
const CHECKED = "☑";
|
|
24
|
+
const UNCHECKED = "☐";
|
|
25
|
+
|
|
26
|
+
// ────────────────────────────────────────────
|
|
27
|
+
// Type helper — embedded selectType
|
|
28
|
+
// ────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface QuestionParamsWithSelect extends QuestionParams {
|
|
31
|
+
selectType?: SelectType;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ────────────────────────────────────────────
|
|
35
|
+
// Legacy single‑question UI (selectType-aware)
|
|
36
|
+
// ────────────────────────────────────────────
|
|
14
37
|
|
|
15
38
|
export async function runQuestionUi(
|
|
16
39
|
ui: ExtensionUIContext,
|
|
17
|
-
params:
|
|
40
|
+
params: QuestionParamsWithSelect,
|
|
18
41
|
): Promise<QuestionUiResult | null> {
|
|
19
|
-
if (params.options.length === 0)
|
|
20
|
-
|
|
42
|
+
if (params.options.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
if (params.selectType === "multi") {
|
|
45
|
+
return runMultiSelectSingleQuestion(ui, params);
|
|
21
46
|
}
|
|
22
47
|
|
|
48
|
+
return runSingleSelectSingleQuestion(ui, params);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Single-select, single question (original behavior) */
|
|
52
|
+
async function runSingleSelectSingleQuestion(
|
|
53
|
+
ui: ExtensionUIContext,
|
|
54
|
+
params: QuestionParams,
|
|
55
|
+
): Promise<QuestionUiResult | null> {
|
|
23
56
|
const options = params.options;
|
|
24
57
|
const customInputIndex = options.length;
|
|
25
58
|
|
|
@@ -29,7 +62,7 @@ export async function runQuestionUi(
|
|
|
29
62
|
let cachedWidth: number | undefined;
|
|
30
63
|
const customInput = new Input();
|
|
31
64
|
|
|
32
|
-
customInput.onSubmit = (value) => {
|
|
65
|
+
customInput.onSubmit = (value: string) => {
|
|
33
66
|
const trimmed = value.trim();
|
|
34
67
|
if (trimmed && optionIndex === customInputIndex) {
|
|
35
68
|
done({ answer: trimmed, wasCustom: true });
|
|
@@ -49,11 +82,7 @@ export async function runQuestionUi(
|
|
|
49
82
|
function selectPresetOption(index: number) {
|
|
50
83
|
const selected = options[index];
|
|
51
84
|
if (!selected) return;
|
|
52
|
-
done({
|
|
53
|
-
answer: selected.label,
|
|
54
|
-
wasCustom: false,
|
|
55
|
-
index: index + 1,
|
|
56
|
-
});
|
|
85
|
+
done({ answer: selected.label, wasCustom: false, index: index + 1 });
|
|
57
86
|
}
|
|
58
87
|
|
|
59
88
|
function handleInput(data: string) {
|
|
@@ -75,9 +104,7 @@ export async function runQuestionUi(
|
|
|
75
104
|
}
|
|
76
105
|
if (matchesKey(data, Key.enter)) {
|
|
77
106
|
const trimmed = customInput.getValue().trim();
|
|
78
|
-
if (trimmed) {
|
|
79
|
-
done({ answer: trimmed, wasCustom: true });
|
|
80
|
-
}
|
|
107
|
+
if (trimmed) done({ answer: trimmed, wasCustom: true });
|
|
81
108
|
return;
|
|
82
109
|
}
|
|
83
110
|
customInput.focused = true;
|
|
@@ -114,54 +141,6 @@ export async function runQuestionUi(
|
|
|
114
141
|
}
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
function renderCustomInputRow(
|
|
118
|
-
prefix: string,
|
|
119
|
-
numberLabel: string,
|
|
120
|
-
availableWidth: number,
|
|
121
|
-
selected: boolean,
|
|
122
|
-
): string[] {
|
|
123
|
-
const numberPrefix = `${numberLabel} `;
|
|
124
|
-
const numberWidth = visibleWidth(prefix + numberPrefix);
|
|
125
|
-
const inputWidth = Math.max(1, availableWidth - numberWidth);
|
|
126
|
-
const value = customInput.getValue();
|
|
127
|
-
|
|
128
|
-
if (selected && value.length > 0) {
|
|
129
|
-
customInput.focused = true;
|
|
130
|
-
const [inputLine = ""] = customInput.render(inputWidth);
|
|
131
|
-
const stripped = inputLine.startsWith("> ")
|
|
132
|
-
? inputLine.slice(2)
|
|
133
|
-
: inputLine;
|
|
134
|
-
return [`${prefix}${numberPrefix}${stripped}`];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (selected) {
|
|
138
|
-
customInput.focused = true;
|
|
139
|
-
const marker = CURSOR_MARKER;
|
|
140
|
-
const firstChar = CUSTOM_INPUT_PLACEHOLDER[0] ?? " ";
|
|
141
|
-
const rest = CUSTOM_INPUT_PLACEHOLDER.slice(1);
|
|
142
|
-
const cursorFirst = `\x1b[7m${firstChar}\x1b[27m`;
|
|
143
|
-
const restPlaceholder = theme.fg("dim", rest);
|
|
144
|
-
const field = `${marker}${cursorFirst}${restPlaceholder}`;
|
|
145
|
-
const padding = " ".repeat(
|
|
146
|
-
Math.max(0, inputWidth - visibleWidth(CUSTOM_INPUT_PLACEHOLDER)),
|
|
147
|
-
);
|
|
148
|
-
return [`${prefix}${numberPrefix}${field}${padding}`];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
customInput.focused = false;
|
|
152
|
-
const display =
|
|
153
|
-
value.length > 0
|
|
154
|
-
? theme.fg("text", value)
|
|
155
|
-
: theme.fg("dim", CUSTOM_INPUT_PLACEHOLDER);
|
|
156
|
-
const padding =
|
|
157
|
-
value.length > 0
|
|
158
|
-
? ""
|
|
159
|
-
: " ".repeat(
|
|
160
|
-
Math.max(0, inputWidth - visibleWidth(CUSTOM_INPUT_PLACEHOLDER)),
|
|
161
|
-
);
|
|
162
|
-
return [`${prefix}${numberPrefix}${display}${padding}`];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
144
|
function render(width: number): string[] {
|
|
166
145
|
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
167
146
|
|
|
@@ -171,7 +150,6 @@ export async function runQuestionUi(
|
|
|
171
150
|
function addWrapped(text: string) {
|
|
172
151
|
lines.push(...wrapTextWithAnsi(text, renderWidth));
|
|
173
152
|
}
|
|
174
|
-
|
|
175
153
|
function addWrappedWithPrefix(prefix: string, text: string) {
|
|
176
154
|
const prefixWidth = visibleWidth(prefix);
|
|
177
155
|
if (prefixWidth >= renderWidth) {
|
|
@@ -179,9 +157,9 @@ export async function runQuestionUi(
|
|
|
179
157
|
return;
|
|
180
158
|
}
|
|
181
159
|
const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
|
|
182
|
-
const
|
|
160
|
+
const cp = " ".repeat(prefixWidth);
|
|
183
161
|
for (let i = 0; i < wrapped.length; i++) {
|
|
184
|
-
lines.push(`${i === 0 ? prefix :
|
|
162
|
+
lines.push(`${i === 0 ? prefix : cp}${wrapped[i]}`);
|
|
185
163
|
}
|
|
186
164
|
}
|
|
187
165
|
|
|
@@ -191,13 +169,11 @@ export async function runQuestionUi(
|
|
|
191
169
|
|
|
192
170
|
for (let i = 0; i < options.length; i++) {
|
|
193
171
|
const opt = options[i];
|
|
194
|
-
const
|
|
195
|
-
const prefix =
|
|
172
|
+
const sel = !isOnCustomInput() && i === optionIndex;
|
|
173
|
+
const prefix = sel ? theme.fg("accent", "> ") : " ";
|
|
196
174
|
const label = `${i + 1}. ${opt.label}`;
|
|
197
|
-
const color =
|
|
198
|
-
|
|
175
|
+
const color = sel ? "accent" : "text";
|
|
199
176
|
addWrappedWithPrefix(prefix, theme.fg(color, label));
|
|
200
|
-
|
|
201
177
|
if (opt.description) {
|
|
202
178
|
addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
|
|
203
179
|
}
|
|
@@ -206,11 +182,13 @@ export async function runQuestionUi(
|
|
|
206
182
|
const customSelected = isOnCustomInput();
|
|
207
183
|
const customPrefix = customSelected ? theme.fg("accent", "> ") : " ";
|
|
208
184
|
const customNumber = `${customInputIndex + 1}.`;
|
|
209
|
-
for (const line of
|
|
185
|
+
for (const line of renderSingleSelectInputRow(
|
|
210
186
|
customPrefix,
|
|
211
187
|
customNumber,
|
|
212
188
|
renderWidth,
|
|
213
189
|
customSelected,
|
|
190
|
+
customInput,
|
|
191
|
+
theme,
|
|
214
192
|
)) {
|
|
215
193
|
addWrapped(line);
|
|
216
194
|
}
|
|
@@ -218,10 +196,7 @@ export async function runQuestionUi(
|
|
|
218
196
|
lines.push("");
|
|
219
197
|
addWrappedWithPrefix(
|
|
220
198
|
" ",
|
|
221
|
-
theme.fg(
|
|
222
|
-
"dim",
|
|
223
|
-
"↑↓ select • 1-9 quick pick • Enter confirm • Esc cancel",
|
|
224
|
-
),
|
|
199
|
+
theme.fg("dim", "↑↓ select • 1-9 quick pick • Enter confirm • Esc cancel"),
|
|
225
200
|
);
|
|
226
201
|
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
227
202
|
|
|
@@ -232,11 +207,731 @@ export async function runQuestionUi(
|
|
|
232
207
|
|
|
233
208
|
return {
|
|
234
209
|
render,
|
|
235
|
-
invalidate: () => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
210
|
+
invalidate: () => { cachedLines = undefined; cachedWidth = undefined; },
|
|
211
|
+
handleInput,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Multi-select, single question */
|
|
217
|
+
async function runMultiSelectSingleQuestion(
|
|
218
|
+
ui: ExtensionUIContext,
|
|
219
|
+
params: QuestionParams,
|
|
220
|
+
): Promise<QuestionUiResult | null> {
|
|
221
|
+
const options = params.options;
|
|
222
|
+
|
|
223
|
+
return ui.custom<QuestionUiResult | null>((tui, theme, _kb, done) => {
|
|
224
|
+
let cursorIndex = 0;
|
|
225
|
+
const selected = new Set<number>();
|
|
226
|
+
const customInput = new Input();
|
|
227
|
+
let customValue: string | null = null;
|
|
228
|
+
|
|
229
|
+
let cachedLines: string[] | undefined;
|
|
230
|
+
let cachedWidth: number | undefined;
|
|
231
|
+
|
|
232
|
+
const ci = options.length; // custom input index
|
|
233
|
+
const lastRow = ci; // custom input is last row; submit via Enter at any position
|
|
234
|
+
|
|
235
|
+
function refresh() {
|
|
236
|
+
cachedLines = undefined;
|
|
237
|
+
cachedWidth = undefined;
|
|
238
|
+
tui.requestRender();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toggleCurrent() {
|
|
242
|
+
if (cursorIndex < ci) {
|
|
243
|
+
if (selected.has(cursorIndex)) {
|
|
244
|
+
selected.delete(cursorIndex);
|
|
245
|
+
} else {
|
|
246
|
+
selected.add(cursorIndex);
|
|
247
|
+
}
|
|
248
|
+
refresh();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function submitSelected() {
|
|
253
|
+
if (selected.size === 0 && !customValue) return; // ignore empty
|
|
254
|
+
const answers: string[] = [];
|
|
255
|
+
const indices: number[] = [];
|
|
256
|
+
for (const idx of [...selected].sort((a, b) => a - b)) {
|
|
257
|
+
answers.push(options[idx]!.label);
|
|
258
|
+
indices.push(idx + 1);
|
|
259
|
+
}
|
|
260
|
+
if (customValue) {
|
|
261
|
+
answers.push(customValue);
|
|
262
|
+
indices.push(-1);
|
|
263
|
+
}
|
|
264
|
+
done({
|
|
265
|
+
answer: answers.join(", "),
|
|
266
|
+
wasCustom: true,
|
|
267
|
+
index: indices[0],
|
|
268
|
+
multiAnswers: answers,
|
|
269
|
+
multiIndices: indices,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleInput(data: string) {
|
|
274
|
+
if (matchesKey(data, Key.escape)) {
|
|
275
|
+
done(null);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (cursorIndex === ci) {
|
|
280
|
+
// On custom input row
|
|
281
|
+
if (matchesKey(data, Key.up)) {
|
|
282
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
283
|
+
customInput.focused = false;
|
|
284
|
+
refresh();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (matchesKey(data, Key.down)) {
|
|
288
|
+
refresh();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (matchesKey(data, Key.space)) {
|
|
292
|
+
refresh();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (matchesKey(data, Key.enter)) {
|
|
296
|
+
const trimmed = customInput.getValue().trim();
|
|
297
|
+
customValue = trimmed || null;
|
|
298
|
+
submitSelected();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
customInput.focused = true;
|
|
302
|
+
customInput.handleInput(data);
|
|
303
|
+
refresh();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// On a preset option row
|
|
308
|
+
if (matchesKey(data, Key.up)) {
|
|
309
|
+
cursorIndex = Math.max(0, cursorIndex - 1);
|
|
310
|
+
customInput.focused = false;
|
|
311
|
+
refresh();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (matchesKey(data, Key.down)) {
|
|
315
|
+
cursorIndex = Math.min(ci, cursorIndex + 1);
|
|
316
|
+
customInput.focused = cursorIndex === ci;
|
|
317
|
+
refresh();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (matchesKey(data, Key.space)) {
|
|
321
|
+
toggleCurrent();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (matchesKey(data, Key.enter)) {
|
|
325
|
+
submitSelected();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function render(width: number): string[] {
|
|
331
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
332
|
+
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
const renderWidth = Math.max(1, width);
|
|
335
|
+
|
|
336
|
+
function addWrapped(text: string) {
|
|
337
|
+
lines.push(...wrapTextWithAnsi(text, renderWidth));
|
|
338
|
+
}
|
|
339
|
+
function addWrappedWithPrefix(prefix: string, text: string) {
|
|
340
|
+
const prefixWidth = visibleWidth(prefix);
|
|
341
|
+
if (prefixWidth >= renderWidth) { addWrapped(prefix + text); return; }
|
|
342
|
+
const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
|
|
343
|
+
const cp = " ".repeat(prefixWidth);
|
|
344
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
345
|
+
lines.push(`${i === 0 ? prefix : cp}${wrapped[i]}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
350
|
+
addWrappedWithPrefix(" ", theme.fg("text", params.question));
|
|
351
|
+
lines.push("");
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i < options.length; i++) {
|
|
354
|
+
const opt = options[i];
|
|
355
|
+
const isFocused = cursorIndex === i;
|
|
356
|
+
const isChecked = selected.has(i);
|
|
357
|
+
const checkMark = isChecked ? CHECKED : UNCHECKED;
|
|
358
|
+
const prefix = isFocused ? theme.fg("accent", "> ") : " ";
|
|
359
|
+
const check = isFocused
|
|
360
|
+
? theme.fg("accent", checkMark)
|
|
361
|
+
: isChecked
|
|
362
|
+
? theme.fg("success", checkMark)
|
|
363
|
+
: theme.fg("muted", checkMark);
|
|
364
|
+
const label = `${i + 1}. ${opt.label}`;
|
|
365
|
+
const labelColor = isFocused ? "accent" : "text";
|
|
366
|
+
addWrappedWithPrefix(`${prefix}${check} `, theme.fg(labelColor, label));
|
|
367
|
+
if (opt.description) {
|
|
368
|
+
addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Custom input row
|
|
373
|
+
const onCustom = cursorIndex === ci;
|
|
374
|
+
const customPrefix = onCustom ? theme.fg("accent", "> ") : " ";
|
|
375
|
+
const customNumber = `${ci + 1}.`;
|
|
376
|
+
for (const line of renderMultiSelectInputRow(
|
|
377
|
+
customPrefix,
|
|
378
|
+
customNumber,
|
|
379
|
+
renderWidth,
|
|
380
|
+
onCustom,
|
|
381
|
+
customInput,
|
|
382
|
+
theme,
|
|
383
|
+
)) {
|
|
384
|
+
addWrapped(line);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Footer
|
|
388
|
+
lines.push("");
|
|
389
|
+
const n = selected.size + (customValue ? 1 : 0);
|
|
390
|
+
const help = n > 0
|
|
391
|
+
? `↑↓ • Space toggle • Enter submit (${n}) • Esc cancel`
|
|
392
|
+
: "↑↓ • Space toggle • Enter submit • Esc cancel";
|
|
393
|
+
addWrappedWithPrefix(" ", theme.fg("dim", help));
|
|
394
|
+
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
395
|
+
|
|
396
|
+
cachedLines = lines;
|
|
397
|
+
cachedWidth = width;
|
|
398
|
+
return lines;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
render,
|
|
403
|
+
invalidate: () => { cachedLines = undefined; cachedWidth = undefined; },
|
|
404
|
+
handleInput,
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ────────────────────────────────────────────
|
|
410
|
+
// Multi‑question tabbed UI (selectType-aware)
|
|
411
|
+
// ────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
export async function runMultiQuestionUi(
|
|
414
|
+
ui: ExtensionUIContext,
|
|
415
|
+
params: MultiQuestionParams,
|
|
416
|
+
): Promise<MultiQuestionUiResult | null> {
|
|
417
|
+
const tabs = params.questions;
|
|
418
|
+
if (tabs.length === 0) return null;
|
|
419
|
+
|
|
420
|
+
return ui.custom<MultiQuestionUiResult | null>((tui, theme, _kb, done) => {
|
|
421
|
+
// ── state ──
|
|
422
|
+
let activeTab = 0;
|
|
423
|
+
const tabInputs: Input[] = tabs.map(() => new Input());
|
|
424
|
+
const tabOptionIndex: number[] = tabs.map(() => 0);
|
|
425
|
+
/** Per-tab selected indices for multi-select */
|
|
426
|
+
const tabSelected: Set<number>[] = tabs.map(() => new Set());
|
|
427
|
+
const tabCustomValues: (string | null)[] = tabs.map(() => null);
|
|
428
|
+
const tabAnswers: (TabAnswer | null)[] = tabs.map(() => null);
|
|
429
|
+
|
|
430
|
+
let cachedLines: string[] | undefined;
|
|
431
|
+
let cachedWidth: number | undefined;
|
|
432
|
+
|
|
433
|
+
for (const inp of tabInputs) {
|
|
434
|
+
inp.onSubmit = (value: string) => {
|
|
435
|
+
const ti = tabInputs.indexOf(inp);
|
|
436
|
+
if (ti === activeTab) handleTabSubmit(ti, value.trim());
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── helpers ──
|
|
441
|
+
function ci(tab: number): number {
|
|
442
|
+
return tabs[tab]?.options.length ?? 0;
|
|
443
|
+
}
|
|
444
|
+
function isSingle(tab: number): boolean {
|
|
445
|
+
return (tabs[tab]?.selectType ?? "single") === "single";
|
|
446
|
+
}
|
|
447
|
+
function isMulti(tab: number): boolean {
|
|
448
|
+
return (tabs[tab]?.selectType ?? "single") === "multi";
|
|
449
|
+
}
|
|
450
|
+
function isOnCustomInput(tab?: number): boolean {
|
|
451
|
+
const t = tab ?? activeTab;
|
|
452
|
+
return tabOptionIndex[t] === ci(t);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function setAnswer(tab: number, ans: TabAnswer) {
|
|
456
|
+
tabAnswers[tab] = ans;
|
|
457
|
+
if (allAnswered()) {
|
|
458
|
+
done(buildResult());
|
|
459
|
+
} else {
|
|
460
|
+
moveToNextUnanswered();
|
|
461
|
+
refresh();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function allAnswered(): boolean {
|
|
466
|
+
return tabAnswers.every((a) => a !== null);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function buildResult(): MultiQuestionUiResult {
|
|
470
|
+
const answers: Record<string, TabAnswer> = {};
|
|
471
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
472
|
+
const tab = tabs[i];
|
|
473
|
+
const key = tab.id ?? `q${i}`;
|
|
474
|
+
const ans = tabAnswers[i];
|
|
475
|
+
if (ans) answers[key] = ans;
|
|
476
|
+
}
|
|
477
|
+
return { answers };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function moveToNextUnanswered() {
|
|
481
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
482
|
+
const next = (activeTab + 1 + i) % tabs.length;
|
|
483
|
+
if (tabAnswers[next] === null) {
|
|
484
|
+
activeTab = next;
|
|
485
|
+
tabInputs[next]!.focused = isOnCustomInput(next);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function refresh() {
|
|
492
|
+
cachedLines = undefined;
|
|
493
|
+
cachedWidth = undefined;
|
|
494
|
+
tui.requestRender();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── tab submit ──
|
|
498
|
+
function handleTabSubmit(tab: number, customVal: string) {
|
|
499
|
+
if (isSingle(tab)) {
|
|
500
|
+
const opt = tabs[tab]?.options[tabOptionIndex[tab]];
|
|
501
|
+
if (tabOptionIndex[tab] < ci(tab) && opt) {
|
|
502
|
+
setAnswer(tab, { answer: opt.label, wasCustom: false, index: tabOptionIndex[tab] + 1 });
|
|
503
|
+
} else if (customVal) {
|
|
504
|
+
setAnswer(tab, { answer: customVal, wasCustom: true });
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
// multi
|
|
508
|
+
const set = tabSelected[tab]!;
|
|
509
|
+
if (customVal) tabCustomValues[tab] = customVal;
|
|
510
|
+
const n = set.size + (tabCustomValues[tab] ? 1 : 0);
|
|
511
|
+
if (n === 0) return; // ignore empty
|
|
512
|
+
const ansLabels: string[] = [];
|
|
513
|
+
const ansIndices: number[] = [];
|
|
514
|
+
for (const idx of [...set].sort((a, b) => a - b)) {
|
|
515
|
+
ansLabels.push(tabs[tab]!.options[idx]!.label);
|
|
516
|
+
ansIndices.push(idx + 1);
|
|
517
|
+
}
|
|
518
|
+
if (tabCustomValues[tab]) {
|
|
519
|
+
ansLabels.push(tabCustomValues[tab]!);
|
|
520
|
+
ansIndices.push(-1);
|
|
521
|
+
}
|
|
522
|
+
setAnswer(tab, { answers: ansLabels, indices: ansIndices });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ── input ──
|
|
527
|
+
function handleInput(data: string) {
|
|
528
|
+
if (matchesKey(data, Key.escape)) {
|
|
529
|
+
done(null);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Tab switching
|
|
534
|
+
if (matchesKey(data, Key.tab)) {
|
|
535
|
+
switchToTab((activeTab + 1) % tabs.length);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (matchesKey(data, Key.shift("tab"))) {
|
|
539
|
+
switchToTab((activeTab - 1 + tabs.length) % tabs.length);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (matchesKey(data, Key.left)) {
|
|
543
|
+
switchToTab((activeTab - 1 + tabs.length) % tabs.length);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (matchesKey(data, Key.right)) {
|
|
547
|
+
switchToTab((activeTab + 1) % tabs.length);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const limit = ci(activeTab);
|
|
552
|
+
const isAnswered = tabAnswers[activeTab] !== null;
|
|
553
|
+
|
|
554
|
+
if (isAnswered) return; // no editing after submit
|
|
555
|
+
|
|
556
|
+
if (isSingle(activeTab)) {
|
|
557
|
+
handleSingleSelectInput(data);
|
|
558
|
+
} else {
|
|
559
|
+
handleMultiSelectInput(data);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function handleSingleSelectInput(data: string) {
|
|
564
|
+
const limit = ci(activeTab);
|
|
565
|
+
|
|
566
|
+
if (isOnCustomInput()) {
|
|
567
|
+
if (matchesKey(data, Key.up)) {
|
|
568
|
+
tabOptionIndex[activeTab] = Math.max(0, tabOptionIndex[activeTab] - 1);
|
|
569
|
+
tabInputs[activeTab]!.focused = false;
|
|
570
|
+
refresh();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (matchesKey(data, Key.down)) {
|
|
574
|
+
refresh();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (matchesKey(data, Key.enter)) {
|
|
578
|
+
handleTabSubmit(activeTab, tabInputs[activeTab]!.getValue().trim());
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
tabInputs[activeTab]!.focused = true;
|
|
582
|
+
tabInputs[activeTab]!.handleInput(data);
|
|
583
|
+
refresh();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (matchesKey(data, Key.up)) {
|
|
588
|
+
tabOptionIndex[activeTab] = Math.max(0, tabOptionIndex[activeTab] - 1);
|
|
589
|
+
tabInputs[activeTab]!.focused = false;
|
|
590
|
+
refresh();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (matchesKey(data, Key.down)) {
|
|
594
|
+
tabOptionIndex[activeTab] = Math.min(limit, tabOptionIndex[activeTab] + 1);
|
|
595
|
+
tabInputs[activeTab]!.focused = tabOptionIndex[activeTab] === limit;
|
|
596
|
+
refresh();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (matchesKey(data, Key.enter)) {
|
|
600
|
+
const opt = tabs[activeTab]?.options[tabOptionIndex[activeTab]];
|
|
601
|
+
if (opt) {
|
|
602
|
+
setAnswer(activeTab, { answer: opt.label, wasCustom: false, index: tabOptionIndex[activeTab] + 1 });
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const key = parseKey(data);
|
|
608
|
+
if (key && /^[1-9]$/.test(key)) {
|
|
609
|
+
const index = Number(key) - 1;
|
|
610
|
+
if (index <= limit) {
|
|
611
|
+
tabOptionIndex[activeTab] = index;
|
|
612
|
+
tabInputs[activeTab]!.focused = index === limit;
|
|
613
|
+
refresh();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function handleMultiSelectInput(data: string) {
|
|
619
|
+
const limit = ci(activeTab);
|
|
620
|
+
|
|
621
|
+
if (isOnCustomInput()) {
|
|
622
|
+
if (matchesKey(data, Key.up)) {
|
|
623
|
+
tabOptionIndex[activeTab] = Math.max(0, tabOptionIndex[activeTab] - 1);
|
|
624
|
+
tabInputs[activeTab]!.focused = false;
|
|
625
|
+
refresh();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (matchesKey(data, Key.down)) {
|
|
629
|
+
refresh();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (matchesKey(data, Key.space)) {
|
|
633
|
+
refresh();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (matchesKey(data, Key.enter)) {
|
|
637
|
+
handleTabSubmit(activeTab, tabInputs[activeTab]!.getValue().trim());
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
tabInputs[activeTab]!.focused = true;
|
|
641
|
+
tabInputs[activeTab]!.handleInput(data);
|
|
642
|
+
refresh();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// On preset option
|
|
647
|
+
if (matchesKey(data, Key.up)) {
|
|
648
|
+
tabOptionIndex[activeTab] = Math.max(0, tabOptionIndex[activeTab] - 1);
|
|
649
|
+
tabInputs[activeTab]!.focused = false;
|
|
650
|
+
refresh();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (matchesKey(data, Key.down)) {
|
|
654
|
+
tabOptionIndex[activeTab] = Math.min(limit, tabOptionIndex[activeTab] + 1);
|
|
655
|
+
tabInputs[activeTab]!.focused = tabOptionIndex[activeTab] === limit;
|
|
656
|
+
refresh();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (matchesKey(data, Key.space)) {
|
|
660
|
+
const idx = tabOptionIndex[activeTab];
|
|
661
|
+
if (tabSelected[activeTab]!.has(idx)) {
|
|
662
|
+
tabSelected[activeTab]!.delete(idx);
|
|
663
|
+
} else {
|
|
664
|
+
tabSelected[activeTab]!.add(idx);
|
|
665
|
+
}
|
|
666
|
+
refresh();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (matchesKey(data, Key.enter)) {
|
|
670
|
+
handleTabSubmit(activeTab, "");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function switchToTab(index: number) {
|
|
676
|
+
if (index === activeTab) return;
|
|
677
|
+
tabInputs[activeTab]!.focused = false;
|
|
678
|
+
activeTab = index;
|
|
679
|
+
tabInputs[activeTab]!.focused = isOnCustomInput();
|
|
680
|
+
refresh();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── render ──
|
|
684
|
+
|
|
685
|
+
function renderTabBar(width: number): string[] {
|
|
686
|
+
if (tabs.length <= 1) return [];
|
|
687
|
+
|
|
688
|
+
const tabLabels: string[] = tabs.map((t, i) => {
|
|
689
|
+
const answered = tabAnswers[i] !== null;
|
|
690
|
+
const check = answered ? " ✔" : "";
|
|
691
|
+
return t.title ?? `Q${i + 1}${check}`;
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const segments: string[] = [];
|
|
695
|
+
let remaining = width;
|
|
696
|
+
|
|
697
|
+
for (let i = 0; i < tabLabels.length; i++) {
|
|
698
|
+
if (remaining <= 3) break;
|
|
699
|
+
const isActive = i === activeTab;
|
|
700
|
+
const prefix = i === 0 ? " " : " ";
|
|
701
|
+
const suffix = " ";
|
|
702
|
+
const tabsLeft = tabLabels.length - i;
|
|
703
|
+
const maxLabelLen = Math.max(4, Math.floor((remaining - 2) / tabsLeft) - 2);
|
|
704
|
+
let label = tabLabels[i]!;
|
|
705
|
+
if (visibleWidth(label) > maxLabelLen) {
|
|
706
|
+
label = label.slice(0, maxLabelLen - 1) + "…";
|
|
707
|
+
}
|
|
708
|
+
const tabContent = `${prefix}${label}${suffix}`;
|
|
709
|
+
const styled = isActive
|
|
710
|
+
? theme.fg("accent", tabContent)
|
|
711
|
+
: tabAnswers[i] !== null
|
|
712
|
+
? theme.fg("success", tabContent)
|
|
713
|
+
: theme.fg("muted", tabContent);
|
|
714
|
+
segments.push(styled);
|
|
715
|
+
remaining -= visibleWidth(tabContent);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (segments.length === 0) return [];
|
|
719
|
+
return [segments.join("")];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function render(width: number): string[] {
|
|
723
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
724
|
+
|
|
725
|
+
const lines: string[] = [];
|
|
726
|
+
const renderWidth = Math.max(1, width);
|
|
727
|
+
const tab = tabs[activeTab]!;
|
|
728
|
+
const options = tab.options;
|
|
729
|
+
const limit = ci(activeTab);
|
|
730
|
+
const isAnswered = tabAnswers[activeTab] !== null;
|
|
731
|
+
const currentIdx = tabOptionIndex[activeTab];
|
|
732
|
+
const isMultiSelect = tab.selectType === "multi";
|
|
733
|
+
|
|
734
|
+
function addWrapped(text: string) {
|
|
735
|
+
lines.push(...wrapTextWithAnsi(text, renderWidth));
|
|
736
|
+
}
|
|
737
|
+
function addWrappedWithPrefix(prefix: string, text: string) {
|
|
738
|
+
const prefixWidth = visibleWidth(prefix);
|
|
739
|
+
if (prefixWidth >= renderWidth) { addWrapped(prefix + text); return; }
|
|
740
|
+
const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
|
|
741
|
+
const cp = " ".repeat(prefixWidth);
|
|
742
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
743
|
+
lines.push(`${i === 0 ? prefix : cp}${wrapped[i]}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Tab bar
|
|
748
|
+
const tabBarLines = renderTabBar(renderWidth);
|
|
749
|
+
if (tabBarLines.length > 0) {
|
|
750
|
+
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
751
|
+
lines.push(...tabBarLines);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Question
|
|
755
|
+
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
756
|
+
const tabTitle = tab.title ? theme.fg("accent", `[${tab.title}]`) : "";
|
|
757
|
+
const questionText = isAnswered
|
|
758
|
+
? theme.fg("success", `✔ ${tab.question}`)
|
|
759
|
+
: theme.fg("text", tab.question);
|
|
760
|
+
addWrappedWithPrefix(tabTitle ? `${tabTitle} ` : " ", questionText);
|
|
761
|
+
lines.push("");
|
|
762
|
+
|
|
763
|
+
// Options
|
|
764
|
+
if (isMultiSelect && isAnswered) {
|
|
765
|
+
// Show selected items
|
|
766
|
+
const ans = tabAnswers[activeTab] as MultiTabAnswer;
|
|
767
|
+
for (let i = 0; i < ans.answers.length; i++) {
|
|
768
|
+
const idx = ans.indices[i];
|
|
769
|
+
const label = idx === -1
|
|
770
|
+
? theme.fg("success", `✔ ${ans.answers[i]}`)
|
|
771
|
+
: theme.fg("success", `✔ ${idx}. ${ans.answers[i]}`);
|
|
772
|
+
addWrappedWithPrefix(" ", label);
|
|
773
|
+
}
|
|
774
|
+
} else if (isMultiSelect) {
|
|
775
|
+
for (let i = 0; i < options.length; i++) {
|
|
776
|
+
const opt = options[i];
|
|
777
|
+
const isFocused = i === currentIdx && !isOnCustomInput();
|
|
778
|
+
const isChecked = tabSelected[activeTab]!.has(i);
|
|
779
|
+
const checkMark = isChecked ? CHECKED : UNCHECKED;
|
|
780
|
+
const prefix = isFocused ? theme.fg("accent", "> ") : " ";
|
|
781
|
+
const check = isFocused
|
|
782
|
+
? theme.fg("accent", checkMark)
|
|
783
|
+
: isChecked
|
|
784
|
+
? theme.fg("success", checkMark)
|
|
785
|
+
: theme.fg("muted", checkMark);
|
|
786
|
+
const label = `${i + 1}. ${opt.label}`;
|
|
787
|
+
const labelColor = isFocused ? "accent" : "text";
|
|
788
|
+
addWrappedWithPrefix(`${prefix}${check} `, theme.fg(labelColor, label));
|
|
789
|
+
if (opt.description) {
|
|
790
|
+
addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
for (let i = 0; i < options.length; i++) {
|
|
795
|
+
const opt = options[i];
|
|
796
|
+
const sel = !isOnCustomInput() && i === currentIdx;
|
|
797
|
+
const disabled = isAnswered;
|
|
798
|
+
const prefix = sel ? theme.fg("accent", "> ") : " ";
|
|
799
|
+
const label = `${i + 1}. ${opt.label}`;
|
|
800
|
+
const color = disabled ? "dim" : sel ? "accent" : "text";
|
|
801
|
+
addWrappedWithPrefix(prefix, theme.fg(color, label));
|
|
802
|
+
if (opt.description) {
|
|
803
|
+
addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Custom input / answer display
|
|
809
|
+
if (isAnswered) {
|
|
810
|
+
if (!isMultiSelect) {
|
|
811
|
+
const ans = tabAnswers[activeTab] as SingleTabAnswer;
|
|
812
|
+
const displayLabel = ans.wasCustom
|
|
813
|
+
? theme.fg("success", `✔ ${ans.answer}`)
|
|
814
|
+
: theme.fg("success", `✔ ${ans.index}. ${ans.answer}`);
|
|
815
|
+
addWrappedWithPrefix(" ", displayLabel);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
const onCustom = isOnCustomInput();
|
|
819
|
+
const customPrefix = onCustom ? theme.fg("accent", "> ") : " ";
|
|
820
|
+
const customNumber = `${limit + 1}.`;
|
|
821
|
+
|
|
822
|
+
if (isMultiSelect) {
|
|
823
|
+
for (const line of renderMultiSelectInputRow(
|
|
824
|
+
customPrefix,
|
|
825
|
+
customNumber,
|
|
826
|
+
renderWidth,
|
|
827
|
+
onCustom,
|
|
828
|
+
tabInputs[activeTab]!,
|
|
829
|
+
theme,
|
|
830
|
+
)) {
|
|
831
|
+
addWrapped(line);
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
for (const line of renderSingleSelectInputRow(
|
|
835
|
+
customPrefix,
|
|
836
|
+
customNumber,
|
|
837
|
+
renderWidth,
|
|
838
|
+
onCustom,
|
|
839
|
+
tabInputs[activeTab]!,
|
|
840
|
+
theme,
|
|
841
|
+
)) {
|
|
842
|
+
addWrapped(line);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Footer
|
|
848
|
+
lines.push("");
|
|
849
|
+
const helpParts: string[] = [];
|
|
850
|
+
if (tabs.length > 1) helpParts.push("Tab/←→ switch");
|
|
851
|
+
if (isMultiSelect) {
|
|
852
|
+
helpParts.push("↑↓");
|
|
853
|
+
helpParts.push("Space toggle");
|
|
854
|
+
const n = tabSelected[activeTab]!.size + (tabCustomValues[activeTab] ? 1 : 0);
|
|
855
|
+
helpParts.push(n > 0 ? `Enter submit (${n})` : "Enter submit");
|
|
856
|
+
} else {
|
|
857
|
+
helpParts.push("↑↓ select");
|
|
858
|
+
helpParts.push("1-9 quick pick");
|
|
859
|
+
if (!isAnswered) helpParts.push("Enter confirm");
|
|
860
|
+
}
|
|
861
|
+
helpParts.push("Esc cancel");
|
|
862
|
+
addWrappedWithPrefix(" ", theme.fg("dim", helpParts.join(" • ")));
|
|
863
|
+
lines.push(theme.fg("accent", "─".repeat(renderWidth)));
|
|
864
|
+
|
|
865
|
+
cachedLines = lines;
|
|
866
|
+
cachedWidth = width;
|
|
867
|
+
return lines;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
render,
|
|
872
|
+
invalidate: () => { cachedLines = undefined; cachedWidth = undefined; },
|
|
239
873
|
handleInput,
|
|
240
874
|
};
|
|
241
875
|
});
|
|
242
876
|
}
|
|
877
|
+
|
|
878
|
+
// ────────────────────────────────────────────
|
|
879
|
+
// Shared input row renderers
|
|
880
|
+
// ────────────────────────────────────────────
|
|
881
|
+
|
|
882
|
+
function renderSingleSelectInputRow(
|
|
883
|
+
prefix: string,
|
|
884
|
+
numberLabel: string,
|
|
885
|
+
availableWidth: number,
|
|
886
|
+
selected: boolean,
|
|
887
|
+
input: Input,
|
|
888
|
+
theme: Theme,
|
|
889
|
+
): string[] {
|
|
890
|
+
const numberPrefix = `${numberLabel} `;
|
|
891
|
+
const numberWidth = visibleWidth(prefix + numberPrefix);
|
|
892
|
+
const inputWidth = Math.max(1, availableWidth - numberWidth);
|
|
893
|
+
const value = input.getValue();
|
|
894
|
+
|
|
895
|
+
if (selected && value.length > 0) {
|
|
896
|
+
input.focused = true;
|
|
897
|
+
const [line = ""] = input.render(inputWidth);
|
|
898
|
+
const stripped = line.startsWith("> ") ? line.slice(2) : line;
|
|
899
|
+
return [`${prefix}${numberPrefix}${stripped}`];
|
|
900
|
+
}
|
|
901
|
+
if (selected) {
|
|
902
|
+
input.focused = true;
|
|
903
|
+
const marker = CURSOR_MARKER;
|
|
904
|
+
const firstChar = CUSTOM_INPUT_PLACEHOLDER[0] ?? " ";
|
|
905
|
+
const rest = CUSTOM_INPUT_PLACEHOLDER.slice(1);
|
|
906
|
+
const cursorFirst = `\x1b[7m${firstChar}\x1b[27m`;
|
|
907
|
+
const restPlaceholder = theme.fg("dim", rest);
|
|
908
|
+
const field = `${marker}${cursorFirst}${restPlaceholder}`;
|
|
909
|
+
const padding = " ".repeat(Math.max(0, inputWidth - visibleWidth(CUSTOM_INPUT_PLACEHOLDER)));
|
|
910
|
+
return [`${prefix}${numberPrefix}${field}${padding}`];
|
|
911
|
+
}
|
|
912
|
+
input.focused = false;
|
|
913
|
+
const display = value.length > 0
|
|
914
|
+
? theme.fg("text", value)
|
|
915
|
+
: theme.fg("dim", CUSTOM_INPUT_PLACEHOLDER);
|
|
916
|
+
const padding = value.length > 0
|
|
917
|
+
? ""
|
|
918
|
+
: " ".repeat(Math.max(0, inputWidth - visibleWidth(CUSTOM_INPUT_PLACEHOLDER)));
|
|
919
|
+
return [`${prefix}${numberPrefix}${display}${padding}`];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function renderMultiSelectInputRow(
|
|
923
|
+
prefix: string,
|
|
924
|
+
numberLabel: string,
|
|
925
|
+
availableWidth: number,
|
|
926
|
+
selected: boolean,
|
|
927
|
+
input: Input,
|
|
928
|
+
theme: Theme,
|
|
929
|
+
): string[] {
|
|
930
|
+
// Same visual as single-select input but renders differently in multi-select context
|
|
931
|
+
return renderSingleSelectInputRow(prefix, numberLabel, availableWidth, selected, input, theme);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
interface Theme {
|
|
935
|
+
fg: (color: string, text: string) => string;
|
|
936
|
+
dim: (text: string) => string;
|
|
937
|
+
}
|