@xynogen/pix-core 0.2.4 → 0.3.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/README.md +24 -21
- package/package.json +11 -17
- package/skills/ask-user/SKILL.md +0 -48
- package/src/commands/agent-sop/agent-sop.ts +0 -58
- package/src/commands/clear/clear.ts +0 -32
- package/src/commands/diff/diff.ts +0 -32
- package/src/commands/models/models.test.ts +0 -95
- package/src/commands/models/models.ts +0 -367
- package/src/commands/models/patch-builtin.test.ts +0 -66
- package/src/commands/models/patch-builtin.ts +0 -120
- package/src/commands/tools.test.ts +0 -15
- package/src/commands/update/update.test.ts +0 -112
- package/src/commands/update/update.ts +0 -271
- package/src/index.ts +0 -45
- package/src/lib/data.ts +0 -33
- package/src/nudge/capability.test.ts +0 -258
- package/src/nudge/capability.ts +0 -189
- package/src/nudge/index.ts +0 -17
- package/src/nudge/tools.test.ts +0 -157
- package/src/nudge/tools.ts +0 -212
- package/src/tool/ask/ask.test.ts +0 -243
- package/src/tool/ask/components.ts +0 -55
- package/src/tool/ask/helpers.ts +0 -77
- package/src/tool/ask/index.ts +0 -130
- package/src/tool/ask/questionnaire.ts +0 -693
- package/src/tool/ask/rpc.ts +0 -84
- package/src/tool/ask/schema.ts +0 -69
- package/src/tool/ask/single-select-layout.test.ts +0 -124
- package/src/tool/ask/single-select-layout.ts +0 -237
- package/src/tool/ask/types.ts +0 -17
- package/src/tool/todo/todo.test.ts +0 -646
- package/src/tool/todo/todo.ts +0 -218
- package/src/tool/toolbox/toolbox.test.ts +0 -314
- package/src/tool/toolbox/toolbox.ts +0 -570
- package/src/ui/diagnostics.ts +0 -145
- package/src/ui/footer.ts +0 -513
- package/src/ui/welcome.test.ts +0 -124
- package/src/ui/welcome.ts +0 -369
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
Container,
|
|
4
|
-
decodeKittyPrintable,
|
|
5
|
-
Editor,
|
|
6
|
-
fuzzyFilter,
|
|
7
|
-
Key,
|
|
8
|
-
type KeybindingsManager,
|
|
9
|
-
Markdown,
|
|
10
|
-
matchesKey,
|
|
11
|
-
Spacer,
|
|
12
|
-
Text,
|
|
13
|
-
type TUI,
|
|
14
|
-
truncateToWidth,
|
|
15
|
-
wrapTextWithAnsi,
|
|
16
|
-
} from "@earendil-works/pi-tui";
|
|
17
|
-
|
|
18
|
-
import { dim, TabBar } from "./components.js";
|
|
19
|
-
import { safeMarkdownTheme, scrollIndicator, sentinelsFor } from "./helpers.js";
|
|
20
|
-
import type { OptionData, Params, QuestionData } from "./schema.js";
|
|
21
|
-
import {
|
|
22
|
-
SENTINEL_CHAT,
|
|
23
|
-
SENTINEL_FREEFORM,
|
|
24
|
-
SENTINEL_NEXT,
|
|
25
|
-
SEPARATOR,
|
|
26
|
-
SPLIT_PANE_MIN_WIDTH,
|
|
27
|
-
} from "./schema.js";
|
|
28
|
-
import type {
|
|
29
|
-
AnswerKind,
|
|
30
|
-
QuestionAnswer,
|
|
31
|
-
QuestionnaireResult,
|
|
32
|
-
} from "./types.js";
|
|
33
|
-
|
|
34
|
-
// ── AskQuestionnaire ───────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export class AskQuestionnaire extends Container {
|
|
37
|
-
private params: Params;
|
|
38
|
-
private tui: TUI;
|
|
39
|
-
private theme: Theme;
|
|
40
|
-
private keybindings: KeybindingsManager;
|
|
41
|
-
private onDone: (result: QuestionnaireResult | null) => void;
|
|
42
|
-
|
|
43
|
-
private currentIndex = 0;
|
|
44
|
-
private answers: QuestionAnswer[] = [];
|
|
45
|
-
private searchQuery = "";
|
|
46
|
-
private selectedOptionIndex = 0;
|
|
47
|
-
private multiChecked = new Set<number>();
|
|
48
|
-
private inputMode = false;
|
|
49
|
-
private editor?: Editor;
|
|
50
|
-
private mdTheme = safeMarkdownTheme();
|
|
51
|
-
|
|
52
|
-
constructor(
|
|
53
|
-
params: Params,
|
|
54
|
-
tui: TUI,
|
|
55
|
-
theme: Theme,
|
|
56
|
-
keybindings: KeybindingsManager,
|
|
57
|
-
onDone: (result: QuestionnaireResult | null) => void,
|
|
58
|
-
) {
|
|
59
|
-
super();
|
|
60
|
-
this.params = params;
|
|
61
|
-
this.tui = tui;
|
|
62
|
-
this.theme = theme;
|
|
63
|
-
this.keybindings = keybindings;
|
|
64
|
-
this.onDone = onDone;
|
|
65
|
-
this.renderLayout();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Accessors ──────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
private get currentQ(): QuestionData {
|
|
71
|
-
return this.params.questions[this.currentIndex]!;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private get filteredOptions(): OptionData[] {
|
|
75
|
-
if (!this.searchQuery) return this.currentQ.options;
|
|
76
|
-
return fuzzyFilter(
|
|
77
|
-
this.currentQ.options,
|
|
78
|
-
this.searchQuery,
|
|
79
|
-
(o) => `${o.label} ${o.description}`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private get mainListItems(): Array<{
|
|
84
|
-
kind: string;
|
|
85
|
-
label?: string;
|
|
86
|
-
option?: OptionData;
|
|
87
|
-
}> {
|
|
88
|
-
const items: Array<{ kind: string; label?: string; option?: OptionData }> =
|
|
89
|
-
[];
|
|
90
|
-
for (const o of this.filteredOptions) {
|
|
91
|
-
items.push({ kind: "option", option: o });
|
|
92
|
-
}
|
|
93
|
-
for (const s of sentinelsFor(this.currentQ)) {
|
|
94
|
-
items.push({ kind: s.kind, label: s.label });
|
|
95
|
-
}
|
|
96
|
-
return items;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private get totalItems(): number {
|
|
100
|
-
return this.mainListItems.length;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private get selectedItem(): (typeof this.mainListItems)[0] | undefined {
|
|
104
|
-
return this.mainListItems[this.selectedOptionIndex];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Layout ─────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
override invalidate(): void {
|
|
110
|
-
super.invalidate();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
renderLayout(): void {
|
|
114
|
-
this.clear();
|
|
115
|
-
const t = this.theme;
|
|
116
|
-
|
|
117
|
-
this.addChild(new Text("", 0, 0));
|
|
118
|
-
|
|
119
|
-
if (this.params.questions.length > 1) {
|
|
120
|
-
this.addChild(new TabBar(this.params.questions, this.currentIndex, t));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const q = this.currentQ;
|
|
124
|
-
const chip = t.fg("accent", t.bold(q.header));
|
|
125
|
-
const prog =
|
|
126
|
-
this.params.questions.length > 1
|
|
127
|
-
? dim(t)(
|
|
128
|
-
scrollIndicator(this.currentIndex, this.params.questions.length),
|
|
129
|
-
)
|
|
130
|
-
: "";
|
|
131
|
-
this.addChild(new Text(`${chip}${prog}`, 1, 0));
|
|
132
|
-
this.addChild(new Spacer(1));
|
|
133
|
-
this.addChild(new Text(t.fg("text", t.bold(q.question)), 1, 0));
|
|
134
|
-
this.addChild(new Spacer(1));
|
|
135
|
-
|
|
136
|
-
if (!q.multiSelect && !this.inputMode) {
|
|
137
|
-
const searchVal = this.searchQuery
|
|
138
|
-
? t.fg("text", this.searchQuery)
|
|
139
|
-
: t.fg("dim", "type to filter");
|
|
140
|
-
this.addChild(
|
|
141
|
-
new Text(`${t.fg("accent", "Filter:")} ${searchVal}`, 1, 0),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.addChild(new Spacer(1));
|
|
146
|
-
|
|
147
|
-
if (this.inputMode) {
|
|
148
|
-
this.addChild(this.ensureEditor());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.addChild(new Spacer(1));
|
|
152
|
-
this.addChild(this._buildHintText());
|
|
153
|
-
this.addChild(new Text("", 0, 0));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private ensureEditor(): Editor {
|
|
157
|
-
if (this.editor) return this.editor;
|
|
158
|
-
const editor = new Editor(this.tui, {
|
|
159
|
-
borderColor: (s: string) => this.theme.fg("accent", s),
|
|
160
|
-
selectList: {
|
|
161
|
-
selectedPrefix: (s: string) => this.theme.fg("accent", s),
|
|
162
|
-
selectedText: (s: string) => this.theme.fg("accent", s),
|
|
163
|
-
description: (s: string) => this.theme.fg("muted", s),
|
|
164
|
-
scrollInfo: (s: string) => this.theme.fg("dim", s),
|
|
165
|
-
noMatch: (s: string) => this.theme.fg("warning", s),
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
editor.disableSubmit = false;
|
|
169
|
-
editor.onSubmit = (text: string) => this.handleFreeformSubmit(text);
|
|
170
|
-
editor.focused = true;
|
|
171
|
-
this.editor = editor;
|
|
172
|
-
return editor;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private _buildHintText(): Text {
|
|
176
|
-
const t = this.theme;
|
|
177
|
-
const isMulti = !!this.currentQ.multiSelect;
|
|
178
|
-
const hints: string[] = [];
|
|
179
|
-
if (this.inputMode) {
|
|
180
|
-
hints.push(dim(t)("enter=submit • esc=back • ^c=cancel"));
|
|
181
|
-
} else {
|
|
182
|
-
const multiQ = this.params.questions.length > 1;
|
|
183
|
-
const nav = multiQ ? "↑↓=nav • ←→=question" : "↑↓=nav";
|
|
184
|
-
if (isMulti) {
|
|
185
|
-
hints.push(
|
|
186
|
-
dim(t)(
|
|
187
|
-
`${nav} • space=toggle • enter=commit & next • esc=clear • ^c=cancel`,
|
|
188
|
-
),
|
|
189
|
-
);
|
|
190
|
-
} else {
|
|
191
|
-
hints.push(
|
|
192
|
-
dim(t)(`${nav} • type=filter • enter=select • esc=clear • ^c=cancel`),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return new Text(hints.join("\n"), 1, 0);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ── Answer management ──────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
private recordAnswer(
|
|
202
|
-
kind: AnswerKind,
|
|
203
|
-
answer: string | null,
|
|
204
|
-
selected?: string[],
|
|
205
|
-
preview?: string,
|
|
206
|
-
): void {
|
|
207
|
-
this.answers = this.answers.filter(
|
|
208
|
-
(a) => a.questionIndex !== this.currentIndex,
|
|
209
|
-
);
|
|
210
|
-
this.answers.push({
|
|
211
|
-
questionIndex: this.currentIndex,
|
|
212
|
-
question: this.currentQ.question,
|
|
213
|
-
kind,
|
|
214
|
-
answer,
|
|
215
|
-
selected,
|
|
216
|
-
preview,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private commitAnswer(): void {
|
|
221
|
-
const item = this.selectedItem;
|
|
222
|
-
if (!item) {
|
|
223
|
-
this.cancel();
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (item.kind === "option" && item.option) {
|
|
228
|
-
this.recordAnswer(
|
|
229
|
-
"option",
|
|
230
|
-
item.option.label,
|
|
231
|
-
undefined,
|
|
232
|
-
item.option.preview,
|
|
233
|
-
);
|
|
234
|
-
this.nextQuestion();
|
|
235
|
-
} else if (item.kind === "other") {
|
|
236
|
-
this.inputMode = true;
|
|
237
|
-
this.ensureEditor().focused = true;
|
|
238
|
-
this.invalidate();
|
|
239
|
-
this.renderLayout();
|
|
240
|
-
this.tui.requestRender();
|
|
241
|
-
} else if (item.kind === "next") {
|
|
242
|
-
const selected = Array.from(this.multiChecked)
|
|
243
|
-
.sort((a, b) => a - b)
|
|
244
|
-
.map((i) => this.currentQ.options[i]?.label);
|
|
245
|
-
if (selected.length === 0) {
|
|
246
|
-
this.cancel();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
this.recordAnswer("multi", null, selected);
|
|
250
|
-
this.nextQuestion();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private handleFreeformSubmit(text: string): void {
|
|
255
|
-
if (!text.trim()) {
|
|
256
|
-
this.cancel();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
this.recordAnswer("custom", text.trim());
|
|
260
|
-
this.nextQuestion();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private gotoQuestion(index: number): void {
|
|
264
|
-
if (index < 0 || index >= this.params.questions.length) return;
|
|
265
|
-
this.currentIndex = index;
|
|
266
|
-
this.searchQuery = "";
|
|
267
|
-
this.multiChecked.clear();
|
|
268
|
-
this.inputMode = false;
|
|
269
|
-
this.selectedOptionIndex = 0;
|
|
270
|
-
this.editor = undefined;
|
|
271
|
-
this.restoreAnswerState();
|
|
272
|
-
this.invalidate();
|
|
273
|
-
this.renderLayout();
|
|
274
|
-
this.tui.requestRender();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private restoreAnswerState(): void {
|
|
278
|
-
const prev = this.answers.find(
|
|
279
|
-
(a) => a.questionIndex === this.currentIndex,
|
|
280
|
-
);
|
|
281
|
-
if (!prev) return;
|
|
282
|
-
const q = this.currentQ;
|
|
283
|
-
if (prev.kind === "multi") {
|
|
284
|
-
for (let i = 0; i < q.options.length; i++) {
|
|
285
|
-
if (prev.selected?.includes(q.options[i]!.label)) {
|
|
286
|
-
this.multiChecked.add(i);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
} else if (prev.kind === "option" && prev.answer) {
|
|
290
|
-
const idx = this.mainListItems.findIndex(
|
|
291
|
-
(it) => it.kind === "option" && it.option?.label === prev.answer,
|
|
292
|
-
);
|
|
293
|
-
if (idx >= 0) this.selectedOptionIndex = idx;
|
|
294
|
-
} else if (prev.kind === "custom") {
|
|
295
|
-
const idx = this.mainListItems.findIndex((it) => it.kind === "other");
|
|
296
|
-
if (idx >= 0) this.selectedOptionIndex = idx;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private nextQuestion(): void {
|
|
301
|
-
const total = this.params.questions.length;
|
|
302
|
-
const answered = new Set(this.answers.map((a) => a.questionIndex));
|
|
303
|
-
for (let step = 1; step <= total; step++) {
|
|
304
|
-
const idx = (this.currentIndex + step) % total;
|
|
305
|
-
if (!answered.has(idx)) {
|
|
306
|
-
this.gotoQuestion(idx);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
this.answers.sort((a, b) => a.questionIndex - b.questionIndex);
|
|
311
|
-
this.onDone({ answers: this.answers, cancelled: false });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private cancel(): void {
|
|
315
|
-
this.onDone({ answers: this.answers, cancelled: true });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
private toggleMulti(index: number): void {
|
|
319
|
-
if (index < 0 || index >= this.currentQ.options.length) return;
|
|
320
|
-
if (this.multiChecked.has(index)) this.multiChecked.delete(index);
|
|
321
|
-
else this.multiChecked.add(index);
|
|
322
|
-
this.invalidate();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ── Input handling ─────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
handleInput(data: string): void {
|
|
328
|
-
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
329
|
-
this.cancel();
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (this.inputMode) {
|
|
334
|
-
if (matchesKey(data, Key.escape)) {
|
|
335
|
-
this.inputMode = false;
|
|
336
|
-
this.editor = undefined;
|
|
337
|
-
this.invalidate();
|
|
338
|
-
this.renderLayout();
|
|
339
|
-
this.tui.requestRender();
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
this.ensureEditor().handleInput(data);
|
|
343
|
-
this.tui.requestRender();
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const isMulti = !!this.currentQ.multiSelect;
|
|
348
|
-
const total = this.totalItems;
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
this.keybindings.matches(data, "tui.select.up") ||
|
|
352
|
-
matchesKey(data, Key.shift("tab")) ||
|
|
353
|
-
matchesKey(data, Key.ctrl("k"))
|
|
354
|
-
) {
|
|
355
|
-
if (total > 0) {
|
|
356
|
-
this.selectedOptionIndex =
|
|
357
|
-
(this.selectedOptionIndex - 1 + total) % total;
|
|
358
|
-
this.invalidate();
|
|
359
|
-
this.tui.requestRender();
|
|
360
|
-
}
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
this.keybindings.matches(data, "tui.select.down") ||
|
|
366
|
-
matchesKey(data, Key.tab) ||
|
|
367
|
-
matchesKey(data, Key.ctrl("j"))
|
|
368
|
-
) {
|
|
369
|
-
if (total > 0) {
|
|
370
|
-
this.selectedOptionIndex = (this.selectedOptionIndex + 1) % total;
|
|
371
|
-
this.invalidate();
|
|
372
|
-
this.tui.requestRender();
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (matchesKey(data, Key.left)) {
|
|
378
|
-
this.gotoQuestion(this.currentIndex - 1);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (matchesKey(data, Key.right)) {
|
|
382
|
-
this.gotoQuestion(this.currentIndex + 1);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
this.keybindings.matches(data, "tui.editor.deleteCharBackward") ||
|
|
388
|
-
matchesKey(data, Key.backspace)
|
|
389
|
-
) {
|
|
390
|
-
if (this.searchQuery) {
|
|
391
|
-
const chars = [...this.searchQuery];
|
|
392
|
-
chars.pop();
|
|
393
|
-
this.searchQuery = chars.join("");
|
|
394
|
-
this.selectedOptionIndex = 0;
|
|
395
|
-
this.invalidate();
|
|
396
|
-
this.tui.requestRender();
|
|
397
|
-
}
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (matchesKey(data, Key.escape)) {
|
|
402
|
-
if (this.searchQuery) {
|
|
403
|
-
this.searchQuery = "";
|
|
404
|
-
this.selectedOptionIndex = 0;
|
|
405
|
-
this.invalidate();
|
|
406
|
-
this.tui.requestRender();
|
|
407
|
-
}
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (matchesKey(data, Key.space) && isMulti) {
|
|
412
|
-
if (this.selectedItem?.kind === "option" && this.selectedItem.option) {
|
|
413
|
-
const idx = this.filteredOptions.indexOf(this.selectedItem.option);
|
|
414
|
-
if (idx >= 0) this.toggleMulti(idx);
|
|
415
|
-
this.invalidate();
|
|
416
|
-
this.tui.requestRender();
|
|
417
|
-
}
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const numMatch = data.match(/^[1-9]$/);
|
|
422
|
-
if (numMatch && this.filteredOptions.length > 0) {
|
|
423
|
-
const idx = Number(numMatch[0]) - 1;
|
|
424
|
-
if (idx >= 0 && idx < this.filteredOptions.length) {
|
|
425
|
-
if (isMulti) {
|
|
426
|
-
this.toggleMulti(idx);
|
|
427
|
-
this.selectedOptionIndex = Math.min(idx, this.totalItems - 1);
|
|
428
|
-
this.invalidate();
|
|
429
|
-
this.tui.requestRender();
|
|
430
|
-
} else {
|
|
431
|
-
const opt = this.filteredOptions[idx]!;
|
|
432
|
-
this.recordAnswer("option", opt.label, undefined, opt.preview);
|
|
433
|
-
this.nextQuestion();
|
|
434
|
-
}
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
440
|
-
this.commitAnswer();
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!isMulti) {
|
|
445
|
-
const printable = decodeKittyPrintable(data);
|
|
446
|
-
if (printable !== undefined) {
|
|
447
|
-
this.searchQuery += printable;
|
|
448
|
-
this.selectedOptionIndex = 0;
|
|
449
|
-
this.invalidate();
|
|
450
|
-
this.tui.requestRender();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const chars = [...data];
|
|
454
|
-
if (
|
|
455
|
-
chars.length === 1 &&
|
|
456
|
-
chars[0] &&
|
|
457
|
-
chars[0].charCodeAt(0) >= 32 &&
|
|
458
|
-
chars[0].charCodeAt(0) < 127
|
|
459
|
-
) {
|
|
460
|
-
this.searchQuery += chars[0];
|
|
461
|
-
this.selectedOptionIndex = 0;
|
|
462
|
-
this.invalidate();
|
|
463
|
-
this.tui.requestRender();
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ── Rendering ──────────────────────────────────────────────────────
|
|
469
|
-
|
|
470
|
-
private renderOptions(width: number): string[] {
|
|
471
|
-
const t = this.theme;
|
|
472
|
-
const inner = Math.max(20, width - 6);
|
|
473
|
-
const isMulti = !!this.currentQ.multiSelect;
|
|
474
|
-
const items = this.mainListItems;
|
|
475
|
-
const total = items.length;
|
|
476
|
-
const chk = (i: number) =>
|
|
477
|
-
isMulti
|
|
478
|
-
? this.multiChecked.has(i)
|
|
479
|
-
? t.fg("success", "✓")
|
|
480
|
-
: t.fg("dim", "○")
|
|
481
|
-
: "";
|
|
482
|
-
|
|
483
|
-
if (total === 0) return [t.fg("warning", "No options")];
|
|
484
|
-
|
|
485
|
-
const maxVisible = Math.min(total, 12);
|
|
486
|
-
const start = Math.max(
|
|
487
|
-
0,
|
|
488
|
-
Math.min(
|
|
489
|
-
this.selectedOptionIndex - Math.floor(maxVisible / 2),
|
|
490
|
-
total - maxVisible,
|
|
491
|
-
),
|
|
492
|
-
);
|
|
493
|
-
const end = Math.min(start + maxVisible, total);
|
|
494
|
-
|
|
495
|
-
const lines: string[] = [];
|
|
496
|
-
const pad = " ";
|
|
497
|
-
|
|
498
|
-
for (let i = start; i < end; i++) {
|
|
499
|
-
const item = items[i]!;
|
|
500
|
-
const sel = i === this.selectedOptionIndex;
|
|
501
|
-
const ptr = sel ? t.fg("accent", "→") : " ";
|
|
502
|
-
|
|
503
|
-
if (item.kind === "option" && item.option) {
|
|
504
|
-
const optIdx = this.filteredOptions.indexOf(item.option);
|
|
505
|
-
const checkbox = isMulti ? ` ${chk(optIdx)}` : "";
|
|
506
|
-
const num = t.fg("dim", `${optIdx + 1}.`);
|
|
507
|
-
const label = sel
|
|
508
|
-
? t.fg("accent", t.bold(item.option.label))
|
|
509
|
-
: t.fg("text", t.bold(item.option.label));
|
|
510
|
-
lines.push(
|
|
511
|
-
truncateToWidth(`${ptr} ${num}${checkbox} ${label}`, inner, ""),
|
|
512
|
-
);
|
|
513
|
-
if (item.option.description) {
|
|
514
|
-
const wrapped = wrapTextWithAnsi(
|
|
515
|
-
item.option.description,
|
|
516
|
-
Math.max(10, inner - 6),
|
|
517
|
-
);
|
|
518
|
-
for (const w of wrapped) {
|
|
519
|
-
lines.push(truncateToWidth(`${pad}${t.fg("muted", w)}`, inner, ""));
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} else if (item.kind === "other") {
|
|
523
|
-
const label = sel
|
|
524
|
-
? t.fg("accent", t.bold(SENTINEL_FREEFORM))
|
|
525
|
-
: t.fg("text", t.bold(SENTINEL_FREEFORM));
|
|
526
|
-
lines.push(
|
|
527
|
-
truncateToWidth(`${ptr} ${t.fg("dim", "✎")} ${label}`, inner, ""),
|
|
528
|
-
);
|
|
529
|
-
} else if (item.kind === "next") {
|
|
530
|
-
const label = sel
|
|
531
|
-
? t.fg("accent", t.bold(SENTINEL_NEXT))
|
|
532
|
-
: t.fg("text", t.bold(SENTINEL_NEXT));
|
|
533
|
-
lines.push(
|
|
534
|
-
truncateToWidth(`${ptr} ${t.fg("dim", "→")} ${label}`, inner, ""),
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (start > 0 || end < total) {
|
|
540
|
-
const count =
|
|
541
|
-
this.filteredOptions.length > 0
|
|
542
|
-
? `${this.selectedOptionIndex + 1}/${total}`
|
|
543
|
-
: `${total}`;
|
|
544
|
-
lines.push(t.fg("dim", truncateToWidth(` ${count}`, inner, "")));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return lines;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private renderPreview(width: number): string[] {
|
|
551
|
-
const item = this.selectedItem;
|
|
552
|
-
if (item?.kind !== "option" || !item.option?.preview) {
|
|
553
|
-
return [this.theme.fg("dim", "No preview")];
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const mdText = item.option.preview;
|
|
557
|
-
const mdWidth = Math.max(10, width);
|
|
558
|
-
|
|
559
|
-
if (this.mdTheme) {
|
|
560
|
-
const md = new Markdown(
|
|
561
|
-
`## ${item.option.label}\n\n${mdText}`,
|
|
562
|
-
0,
|
|
563
|
-
0,
|
|
564
|
-
this.mdTheme,
|
|
565
|
-
);
|
|
566
|
-
return md.render(mdWidth);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const lines = wrapTextWithAnsi(mdText, mdWidth);
|
|
570
|
-
return lines.map((l) =>
|
|
571
|
-
truncateToWidth(this.theme.fg("muted", l), mdWidth, ""),
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
override render(width: number): string[] {
|
|
576
|
-
const inner = Math.max(20, width - 4);
|
|
577
|
-
const t = this.theme;
|
|
578
|
-
const isMulti = !!this.currentQ.multiSelect;
|
|
579
|
-
const hasPreview =
|
|
580
|
-
!isMulti &&
|
|
581
|
-
this.selectedItem?.kind === "option" &&
|
|
582
|
-
!!this.selectedItem?.option?.preview;
|
|
583
|
-
|
|
584
|
-
const useSplit = hasPreview && width >= SPLIT_PANE_MIN_WIDTH;
|
|
585
|
-
const leftWidth = useSplit ? Math.floor((width - 6) * 0.45) : inner;
|
|
586
|
-
const previewWidth = useSplit ? Math.max(20, width - leftWidth - 10) : 0;
|
|
587
|
-
|
|
588
|
-
const lines: string[] = [];
|
|
589
|
-
|
|
590
|
-
const row = (content: string): string =>
|
|
591
|
-
` ${truncateToWidth(content, Math.max(0, width - 1), "")}`;
|
|
592
|
-
|
|
593
|
-
// Tab bar
|
|
594
|
-
if (this.params.questions.length > 1) {
|
|
595
|
-
const tabParts: string[] = [];
|
|
596
|
-
for (let i = 0; i < this.params.questions.length; i++) {
|
|
597
|
-
const active = i === this.currentIndex;
|
|
598
|
-
const tag = `${i + 1}.${this.params.questions[i]?.header}`;
|
|
599
|
-
tabParts.push(active ? t.fg("accent", t.bold(tag)) : t.fg("dim", tag));
|
|
600
|
-
}
|
|
601
|
-
lines.push(row(tabParts.join(t.fg("dim", " "))));
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Header chip
|
|
605
|
-
const chip = t.fg("accent", t.bold(this.currentQ.header));
|
|
606
|
-
const prog =
|
|
607
|
-
this.params.questions.length > 1
|
|
608
|
-
? dim(t)(` ${this.currentIndex + 1}/${this.params.questions.length}`)
|
|
609
|
-
: "";
|
|
610
|
-
lines.push(row(`${chip}${prog}`));
|
|
611
|
-
|
|
612
|
-
// Question text
|
|
613
|
-
for (const w of wrapTextWithAnsi(
|
|
614
|
-
this.currentQ.question,
|
|
615
|
-
Math.max(10, inner),
|
|
616
|
-
)) {
|
|
617
|
-
lines.push(row(t.fg("text", t.bold(w))));
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Input mode
|
|
621
|
-
if (this.inputMode) {
|
|
622
|
-
lines.push("");
|
|
623
|
-
lines.push(row(t.fg("accent", t.bold("Type your response:"))));
|
|
624
|
-
lines.push("");
|
|
625
|
-
const editorLines = this.ensureEditor().render(Math.max(0, width - 1));
|
|
626
|
-
for (const el of editorLines) {
|
|
627
|
-
lines.push(` ${truncateToWidth(el, Math.max(0, width - 1), "")}`);
|
|
628
|
-
}
|
|
629
|
-
lines.push("");
|
|
630
|
-
lines.push(row(dim(t)("enter submit • esc back • ctrl+c cancel")));
|
|
631
|
-
lines.push("");
|
|
632
|
-
return lines.map((l) => truncateToWidth(l, width, ""));
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Search bar
|
|
636
|
-
if (!isMulti) {
|
|
637
|
-
const searchVal = this.searchQuery
|
|
638
|
-
? t.fg("text", this.searchQuery)
|
|
639
|
-
: t.fg("dim", "type to filter");
|
|
640
|
-
lines.push(row(`${t.fg("accent", "Filter:")} ${searchVal}`));
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Chat sentinel
|
|
644
|
-
const chatLabel =
|
|
645
|
-
this.selectedOptionIndex === -999
|
|
646
|
-
? t.fg("accent", t.bold(SENTINEL_CHAT))
|
|
647
|
-
: t.fg("dim", SENTINEL_CHAT);
|
|
648
|
-
lines.push(row(` ${t.fg("dim", "💬")} ${chatLabel}`));
|
|
649
|
-
|
|
650
|
-
// Options (with optional split-pane preview)
|
|
651
|
-
const optionLines = this.renderOptions(useSplit ? leftWidth : width - 4);
|
|
652
|
-
const previewLines = useSplit ? this.renderPreview(previewWidth) : [];
|
|
653
|
-
const maxOptLines = Math.max(optionLines.length, previewLines.length);
|
|
654
|
-
|
|
655
|
-
if (useSplit) {
|
|
656
|
-
const sep = t.fg("dim", SEPARATOR);
|
|
657
|
-
for (let i = 0; i < maxOptLines; i++) {
|
|
658
|
-
const left = truncateToWidth(
|
|
659
|
-
optionLines[i] ?? "",
|
|
660
|
-
leftWidth - 1,
|
|
661
|
-
"",
|
|
662
|
-
true,
|
|
663
|
-
);
|
|
664
|
-
const right = truncateToWidth(
|
|
665
|
-
previewLines[i] ?? "",
|
|
666
|
-
previewWidth - 2,
|
|
667
|
-
"",
|
|
668
|
-
);
|
|
669
|
-
const body = `${left || " ".repeat(leftWidth - 1)}${sep}${right || " ".repeat(previewWidth - 2)}`;
|
|
670
|
-
lines.push(` ${truncateToWidth(body, Math.max(0, width - 1), "")}`);
|
|
671
|
-
}
|
|
672
|
-
} else {
|
|
673
|
-
for (const line of optionLines) lines.push(row(line));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Footer hints
|
|
677
|
-
const navHint =
|
|
678
|
-
this.params.questions.length > 1 ? "↑↓ nav • ←→ question" : "↑↓ nav";
|
|
679
|
-
const hintParts = isMulti
|
|
680
|
-
? [
|
|
681
|
-
`${navHint} • space toggle • enter commit • esc clear`,
|
|
682
|
-
"ctrl+c cancel",
|
|
683
|
-
]
|
|
684
|
-
: [
|
|
685
|
-
`${navHint} • type filter • enter select • esc clear`,
|
|
686
|
-
"ctrl+c cancel",
|
|
687
|
-
];
|
|
688
|
-
lines.push(row(dim(t)(hintParts.join(" • "))));
|
|
689
|
-
lines.push("");
|
|
690
|
-
|
|
691
|
-
return lines.map((l) => truncateToWidth(l, width, ""));
|
|
692
|
-
}
|
|
693
|
-
}
|