@xynogen/pix-core 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +11 -17
  2. package/skills/ask-user/SKILL.md +0 -48
  3. package/src/commands/agent-sop/agent-sop.ts +0 -58
  4. package/src/commands/clear/clear.ts +0 -32
  5. package/src/commands/diff/diff.ts +0 -32
  6. package/src/commands/models/models.test.ts +0 -95
  7. package/src/commands/models/models.ts +0 -367
  8. package/src/commands/models/patch-builtin.test.ts +0 -66
  9. package/src/commands/models/patch-builtin.ts +0 -120
  10. package/src/commands/tools.test.ts +0 -15
  11. package/src/commands/update/update.test.ts +0 -112
  12. package/src/commands/update/update.ts +0 -271
  13. package/src/index.ts +0 -45
  14. package/src/lib/data.ts +0 -33
  15. package/src/nudge/capability.test.ts +0 -258
  16. package/src/nudge/capability.ts +0 -189
  17. package/src/nudge/index.ts +0 -17
  18. package/src/nudge/tools.test.ts +0 -157
  19. package/src/nudge/tools.ts +0 -212
  20. package/src/tool/ask/ask.test.ts +0 -243
  21. package/src/tool/ask/components.ts +0 -55
  22. package/src/tool/ask/helpers.ts +0 -77
  23. package/src/tool/ask/index.ts +0 -130
  24. package/src/tool/ask/questionnaire.ts +0 -693
  25. package/src/tool/ask/rpc.ts +0 -84
  26. package/src/tool/ask/schema.ts +0 -69
  27. package/src/tool/ask/single-select-layout.test.ts +0 -124
  28. package/src/tool/ask/single-select-layout.ts +0 -237
  29. package/src/tool/ask/types.ts +0 -17
  30. package/src/tool/todo/todo.test.ts +0 -646
  31. package/src/tool/todo/todo.ts +0 -218
  32. package/src/tool/toolbox/toolbox.test.ts +0 -314
  33. package/src/tool/toolbox/toolbox.ts +0 -570
  34. package/src/ui/diagnostics.ts +0 -145
  35. package/src/ui/footer.ts +0 -512
  36. package/src/ui/welcome.test.ts +0 -124
  37. 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
- }