@vincemakes/kiso-tui-cells 0.46.1 → 0.47.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.
- package/dist/approval-panel.d.ts +149 -62
- package/dist/approval-panel.js +346 -227
- package/dist/components.d.ts +151 -21
- package/dist/components.js +1190 -821
- package/dist/diff.d.ts +40 -3
- package/dist/diff.js +142 -5
- package/dist/graphite.d.ts +123 -0
- package/dist/graphite.js +257 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -3
- package/dist/md.d.ts +18 -7
- package/dist/md.js +290 -37
- package/dist/render.d.ts +136 -27
- package/dist/render.js +381 -137
- package/dist/strings.d.ts +47 -46
- package/dist/strings.js +130 -171
- package/package.json +1 -1
package/dist/approval-panel.js
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
* FOLD (a bounded block's body — content folds, metadata cuts).
|
|
26
26
|
*/
|
|
27
27
|
import { displayWidth } from "./width.js";
|
|
28
|
-
import { cutLine, diffBody, gutterFold, selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
28
|
+
import { atEmbed, bandKeyRow, bandVisible, bandWindow, boxBottom, cutLine, diffBody, foldAtSpaces, foldWords, goldHits, gutterFold, moreMark, selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
29
29
|
// TUI2-R2pre ④: strings.js takes only a TYPE from this module, so the
|
|
30
30
|
// import is erased at compile time and no runtime cycle exists.
|
|
31
|
-
import { displayVerb } from "./strings.js";
|
|
31
|
+
import { bandHeader, displayVerb } from "./strings.js";
|
|
32
32
|
import { escapeTerminal, palette } from "./render.js";
|
|
33
33
|
/** The copy a failed ask owes the human. One line, dim, and it says what
|
|
34
34
|
* is still true rather than what went wrong: the original choices are
|
|
@@ -216,6 +216,41 @@ export function stepLevel(o, from, dir) {
|
|
|
216
216
|
}
|
|
217
217
|
return from;
|
|
218
218
|
}
|
|
219
|
+
export function pickList(spec, state) {
|
|
220
|
+
const query = spec.input === "filter" ? (state.query ?? "") : "";
|
|
221
|
+
const lq = query.toLowerCase();
|
|
222
|
+
const shown = [];
|
|
223
|
+
const hits = new Map();
|
|
224
|
+
spec.options.forEach((o, i) => {
|
|
225
|
+
if (lq === "") {
|
|
226
|
+
shown.push(i);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// the label, then the first column (a profile's host), then the
|
|
230
|
+
// texts the row does not draw (its name) — the first that holds
|
|
231
|
+
// the query is where the gold goes
|
|
232
|
+
const drawn = [o.label, ...(o.cols ?? []).slice(0, 1)];
|
|
233
|
+
for (let c = 0; c < drawn.length; c += 1) {
|
|
234
|
+
const at = atEmbed(drawn[c].toLowerCase(), lq);
|
|
235
|
+
if (at !== null) {
|
|
236
|
+
shown.push(i);
|
|
237
|
+
hits.set(i, { col: c, at });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if ((o.match ?? []).some((m) => atEmbed(m.toLowerCase(), lq) !== null))
|
|
242
|
+
shown.push(i);
|
|
243
|
+
});
|
|
244
|
+
const typed = query.trim();
|
|
245
|
+
const direct = spec.direct === true && shown.length === 0 && /^[^/\s]+\/\S+$/.test(typed) ? typed : null;
|
|
246
|
+
if (direct !== null)
|
|
247
|
+
shown.push(spec.options.length);
|
|
248
|
+
// a cursor the filter took away lands on the first row left, and the
|
|
249
|
+
// level comes with the row it lands on
|
|
250
|
+
const cursor = shown.includes(state.cursor) ? state.cursor : (shown[0] ?? -1);
|
|
251
|
+
const level = cursor === state.cursor ? state.level : startLevel(spec.options[cursor]);
|
|
252
|
+
return { shown, cursor, level, hits, direct, query };
|
|
253
|
+
}
|
|
219
254
|
/** The rule line's text — the why-asked line (the R3 chain): the tool
|
|
220
255
|
* name, the first non-abstain speaker, the fix hint (the §3.5 table,
|
|
221
256
|
* code-accented). The simple flavor carries the CLI's own question
|
|
@@ -325,39 +360,85 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
325
360
|
// block, and none of them the composer's. A rule SEPARATES, a gutter
|
|
326
361
|
// SCOPES — the args keep their gutter because they are a verbatim
|
|
327
362
|
// block; everything that was drawing a boundary is one rule now.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
rows
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
363
|
+
// Graphite P1b: kiso's own question names the band itself — the
|
|
364
|
+
// question, its facts dim — and says its sentence once, at the content
|
|
365
|
+
// edge. The approval's three rows (the rule, the title, the divider)
|
|
366
|
+
// would say it three times: the rule line WAS the question, the title
|
|
367
|
+
// its facts, and the args its sentence again in a gutter that means
|
|
368
|
+
// "quoted verbatim".
|
|
369
|
+
const asked = view.asked;
|
|
370
|
+
const prose = asked === undefined ? [] : foldWords(escapeTerminal(asked.prose), Math.max(1, W - 4)).map((r) => `${gutter}${r}`);
|
|
371
|
+
// Graphite P4 (owner, 2026-10-04): an approval says the call ONCE, as its
|
|
372
|
+
// card will read in the transcript (`SHELL rm -rf build`), and the band's
|
|
373
|
+
// name carries who asked. DECLARED REVERSAL of the `<tool> needs approval
|
|
374
|
+
// — asked by <speaker>` row, of the bold title under it and of the blank
|
|
375
|
+
// row after them (R3a, TUI2-R1.5 ⑤). The body follows only when it adds
|
|
376
|
+
// something: a diff, JSON, or a command the head row cannot show whole.
|
|
377
|
+
const verb = escapeTerminal(displayVerb(view.name)).toUpperCase();
|
|
378
|
+
const verbHead = `${p.bold}${verb}${p.reset}${view.title === "" ? "" : ` ${escapeTerminal(view.title)}`}`;
|
|
379
|
+
const sayOnce = view.flavor === "approval" && asked === undefined;
|
|
380
|
+
// a call that is ONE line of text (a shell command) and is its own title:
|
|
381
|
+
// said once even when the row cannot hold it — the head row folds, its
|
|
382
|
+
// continuation rows under the command's first cell, breaking at a space
|
|
383
|
+
// where it can (the owner's capture, 2026-10-05: cut in the head and then
|
|
384
|
+
// repeated whole in a body, the first half read twice, `sessi|ons` split)
|
|
385
|
+
const oneLine = sayOnce && view.args.kind === "text" && view.args.lines.length === 1 && view.args.lines[0] === view.title;
|
|
386
|
+
const headRoom = Math.max(1, W - 2);
|
|
387
|
+
// (one cell of margin at the right, as the other rows keep)
|
|
388
|
+
const folds = oneLine && visibleWidth(verbHead) > headRoom && headRoom - verb.length - 2 >= 12 ? foldAtSpaces(escapeTerminal(view.title), headRoom - verb.length - 2) : null;
|
|
389
|
+
if (asked !== undefined) {
|
|
390
|
+
rows.push(bandHeader(askedLabel(asked.question, asked.facts, W), W));
|
|
391
|
+
rows.push(...prose);
|
|
392
|
+
}
|
|
393
|
+
else if (sayOnce) {
|
|
394
|
+
const facts = [...(view.amended === true ? ["amended"] : []), `asked by ${escapeTerminal(view.speaker)}`];
|
|
395
|
+
rows.push(bandHeader(`needs you \u00b7 ${facts.join(" \u00b7 ")}`, W));
|
|
396
|
+
rows.push(folds === null ? ` ${cutLine(verbHead, headRoom)}` : ` ${p.bold}${verb}${p.reset} ${folds[0]}`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// a question with no layout of its own (no caller in the product
|
|
400
|
+
// draws one since P1b): the old head, kept for that case
|
|
401
|
+
rows.push(bandHeader("needs you", W));
|
|
402
|
+
rows.push(` ${cutLine(panelRuleText(view), Math.max(1, W - 2))}`);
|
|
403
|
+
rows.push(` ${cutLine(`${p.bold}${escapeTerminal(view.title)}${p.reset}`, Math.max(1, W - 2))}`);
|
|
404
|
+
rows.push("");
|
|
405
|
+
}
|
|
336
406
|
// the args — the bounded block's body: fold, then cap. The └ cut is
|
|
337
407
|
// ONE row (the W20 discipline): when the args exceed the budget, one
|
|
338
408
|
// notice row carries the count and where the rest is (the event log).
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
409
|
+
const repeats = oneLine && (visibleWidth(verbHead) <= headRoom || folds !== null);
|
|
410
|
+
// the folded command's continuation rows ride the args' budget, so a
|
|
411
|
+
// command taller than the screen still ends in the honest cut notice
|
|
412
|
+
const args = repeats
|
|
413
|
+
? (folds ?? []).slice(1).map((r) => ` ${" ".repeat(verb.length + 1)}${r}`)
|
|
414
|
+
: view.args.kind === "diff"
|
|
415
|
+
? diffBody(view.args.diff, W, true) // the expanded path — never the tool cell's capped copy
|
|
416
|
+
: view.args.lines.flatMap((line) => gutterFold(`${p.dim}│${p.reset} `, escapeTerminal(line), W));
|
|
417
|
+
// the speaker's fix hint (`/mode accept-edits approves edits`) rode the
|
|
418
|
+
// end of the retired rule row and was cut there; it has a row of its own
|
|
419
|
+
const hintRow = sayOnce && view.hint !== undefined && view.hint !== "" ? [`${gutter}${cutLine(`${p.dim}${escapeTerminal(view.hint)}${p.reset}`, Math.max(1, W - 2))}`] : [];
|
|
420
|
+
// the amend phase says where the note goes, where the person is looking —
|
|
421
|
+
// it was the status row's, and the bar stays under the panel now
|
|
422
|
+
const amendRow = phase === "amend" ? [`${gutter}${cutLine(`${p.dim}your note goes to the model \u2014 it will propose a new call${p.reset}`, Math.max(1, W - 2))}`] : [];
|
|
423
|
+
// TUI2-R3v2 ①: the block spends N rows on options instead of one, so
|
|
424
|
+
// the args and the list SHARE what is left after the chrome. The list
|
|
425
|
+
// wins the tie: a human at an approval is choosing, and one more line
|
|
426
|
+
// of a command they can also read in the event log is worth less than
|
|
427
|
+
// the row that carries the choice. The args keep a floor of one row so
|
|
428
|
+
// the block never claims to show what it is asking about and then
|
|
429
|
+
// shows nothing.
|
|
350
430
|
const chrome =
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
431
|
+
// P4: the band row, the call's head and the key row (no blank, no
|
|
432
|
+
// rule under it); kiso's own question: the band row, its sentences,
|
|
433
|
+
// the key row; the old head: four rows and the key row
|
|
434
|
+
(asked === undefined ? (sayOnce ? 3 : 5) : 2 + prose.length) +
|
|
435
|
+
hintRow.length +
|
|
436
|
+
amendRow.length +
|
|
356
437
|
(phase === "options" && note !== undefined ? 1 : 0) +
|
|
357
438
|
(view.riskHint !== undefined && view.riskHint !== "" ? 1 : 0) +
|
|
358
439
|
(phase === "asking" ? 1 : 0) +
|
|
359
|
-
// the safer list's rows + its way-back row
|
|
360
|
-
(phase === "safer" && safer !== undefined ? safer.options.length +
|
|
440
|
+
// the safer list's rows + its way-back row + the row that says so
|
|
441
|
+
(phase === "safer" && safer !== undefined ? safer.options.length + 2 : 0);
|
|
361
442
|
const optionCount = phase === "options" ? panelOptions(view).length : 0;
|
|
362
443
|
const optionsShown = Math.min(optionCount, Math.max(1, maxRows - chrome - 1));
|
|
363
444
|
const argsBudget = Math.max(1, maxRows - chrome - optionsShown);
|
|
@@ -385,6 +466,7 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
385
466
|
const risk = view.riskHint;
|
|
386
467
|
if (risk !== undefined && risk !== "")
|
|
387
468
|
rows.push(`${gutter}${cutLine(`${p.warn}${escapeTerminal(risk)}${p.reset}`, Math.max(1, W - 2))}`);
|
|
469
|
+
rows.push(...hintRow, ...amendRow);
|
|
388
470
|
// TUI2-R3v2 ①: the option LIST. While the typed phase is open the list
|
|
389
471
|
// stands down — the human is writing prose to the model, and a bar
|
|
390
472
|
// hovering over "Yes, run it" while they do it claims a choice is still
|
|
@@ -403,6 +485,9 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
403
485
|
// last row and is always present: an alternatives list you cannot back
|
|
404
486
|
// out of would be a trap.
|
|
405
487
|
if (phase === "safer" && safer !== undefined) {
|
|
488
|
+
// Graphite P4: what happened, said in the block — it was the status
|
|
489
|
+
// row's, and the bar stays under the panel now
|
|
490
|
+
rows.push(`${gutter}${cutLine(`${p.dim}asked the model for safer options${p.reset}`, Math.max(1, W - 2))}`);
|
|
406
491
|
offset = rows.length;
|
|
407
492
|
// R2: the `why` takes a COLUMN rather than running on after an em
|
|
408
493
|
// dash — the commands are what is being chosen between, and they
|
|
@@ -438,18 +523,46 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
438
523
|
count: phase === "options" ? optionsShown : phase === "safer" && safer !== undefined ? safer.options.length + 1 : 0,
|
|
439
524
|
first,
|
|
440
525
|
};
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// else in the product, so a CAPPED panel emitted two elbow rows in a
|
|
448
|
-
// row meaning entirely different things. The rule reads as an edge,
|
|
449
|
-
// and the cut notice above it reads as a notice.
|
|
450
|
-
rows.push(`${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`);
|
|
526
|
+
// Graphite P4 (owner, 2026-10-04): one key row closes the band, with the
|
|
527
|
+
// selection's place at the right margin on a list (§8.2); the composer's
|
|
528
|
+
// rail sits under it. DECLARED REVERSAL of the panel's own bottom rule
|
|
529
|
+
// (TUI2-R1.5 ⑪, R2), which doubled the composer's.
|
|
530
|
+
const list = phase === "options" ? { at: cursor, of: panelOptions(view).length } : phase === "safer" && safer !== undefined ? { at: safer.cursor, of: safer.options.length + 1 } : null;
|
|
531
|
+
rows.push(panelKeyRow(panelAffordance(view, phase, cursor, safer), list, W));
|
|
451
532
|
return { rows, ...layout };
|
|
452
533
|
}
|
|
534
|
+
/** Graphite P4's fold for a one-line command; it lives with the card's
|
|
535
|
+
* head now, which folds a command the same way (the card round). */
|
|
536
|
+
export { foldAtSpaces };
|
|
537
|
+
/** The key row: the keys, cut by cells, and the counter at the right margin. */
|
|
538
|
+
export function panelKeyRow(keys, list, W) {
|
|
539
|
+
const p = palette();
|
|
540
|
+
const count = list === null ? "" : `${list.at + 1}/${list.of}`;
|
|
541
|
+
const room = Math.max(1, W - 1 - 2 - (count === "" ? 0 : count.length + 2));
|
|
542
|
+
const text = cutLine(keys, room);
|
|
543
|
+
const gap = count === "" ? "" : " ".repeat(Math.max(2, W - 1 - 2 - visibleWidth(text) - count.length));
|
|
544
|
+
return ` ${p.dim}${text}${gap}${count}${p.reset}`;
|
|
545
|
+
}
|
|
546
|
+
/** Graphite P4: kiso's own question names the band with its facts; when the
|
|
547
|
+
* label does not fit, the LAST fact (a path) is cut from the left, so its
|
|
548
|
+
* end — the folder's own name — and the rule after it stay on screen. */
|
|
549
|
+
function askedLabel(question, facts, W) {
|
|
550
|
+
const room = Math.max(1, W - 8); // `─── ` before the label, ` ───` after it
|
|
551
|
+
const whole = [question, ...facts].join(" \u00b7 ");
|
|
552
|
+
if (visibleWidth(whole) <= room || facts.length === 0)
|
|
553
|
+
return whole;
|
|
554
|
+
const head = [question, ...facts.slice(0, -1)].join(" \u00b7 ");
|
|
555
|
+
const left = room - visibleWidth(head) - 3 - 1; // ` · ` and the ellipsis
|
|
556
|
+
if (left < 4)
|
|
557
|
+
return whole; // too narrow to say anything useful of it — the band cuts as it always did
|
|
558
|
+
let tail = "";
|
|
559
|
+
for (const ch of [...facts[facts.length - 1]].reverse()) {
|
|
560
|
+
if (visibleWidth(ch + tail) > left)
|
|
561
|
+
break;
|
|
562
|
+
tail = ch + tail;
|
|
563
|
+
}
|
|
564
|
+
return `${head} \u00b7 \u2026${tail}`;
|
|
565
|
+
}
|
|
453
566
|
/**
|
|
454
567
|
* The input row's lead. In the options phase there is NOTHING to type,
|
|
455
568
|
* so the lead stops pretending there is.
|
|
@@ -461,14 +574,11 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
461
574
|
* a human really is writing — leads with the word for what they are
|
|
462
575
|
* writing.
|
|
463
576
|
*/
|
|
464
|
-
export function panelLead(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// nothing is eight bytes on the composer row of every frame a panel
|
|
470
|
-
// is up, and the row it wraps has no content to style.
|
|
471
|
-
return PANEL_IDLE_LEAD === "" ? "" : `${p.dim}${PANEL_IDLE_LEAD}${p.reset}`;
|
|
577
|
+
export function panelLead(_view, _phase, _cursor) {
|
|
578
|
+
// Graphite P4 (owner, 2026-10-04): a panel leaves the input row to the
|
|
579
|
+
// composer in every phase — DECLARED REVERSAL of the named `amend›` lead;
|
|
580
|
+
// the empty note row carries a dim hint instead (the tui's panelHintOf)
|
|
581
|
+
return PANEL_IDLE_LEAD;
|
|
472
582
|
}
|
|
473
583
|
/** The composer's lead while a selection list owns the keys.
|
|
474
584
|
*
|
|
@@ -483,8 +593,8 @@ const PANEL_IDLE_LEAD = "";
|
|
|
483
593
|
/** The lead's plain text — the editor's reflow width (the line must
|
|
484
594
|
* fit the lead + the drawn cursor's own cell — R2 retired the box and
|
|
485
595
|
* its walls with it). */
|
|
486
|
-
export function panelLeadPlain(
|
|
487
|
-
return
|
|
596
|
+
export function panelLeadPlain(_view, _phase, _cursor) {
|
|
597
|
+
return PANEL_IDLE_LEAD;
|
|
488
598
|
}
|
|
489
599
|
export function panelLeadWidth(view, phase, cursor) {
|
|
490
600
|
return displayWidth(panelLeadPlain(view, phase, cursor));
|
|
@@ -521,8 +631,9 @@ export function panelStatus(view, phase, cursor) {
|
|
|
521
631
|
* arrows are — an affordance nobody is told about is one nobody uses.
|
|
522
632
|
*/
|
|
523
633
|
export function panelAffordance(view, phase, cursor, safer) {
|
|
634
|
+
// Graphite P4: the amend keys say where esc goes back to
|
|
524
635
|
if (phase === "amend")
|
|
525
|
-
return "⏎
|
|
636
|
+
return "⏎ sends · esc back to the choices";
|
|
526
637
|
// TUI2-R3v2 ③: the ask is in flight — the ONE key that still means
|
|
527
638
|
// something is the one that gets you out of it.
|
|
528
639
|
if (phase === "asking")
|
|
@@ -530,205 +641,199 @@ export function panelAffordance(view, phase, cursor, safer) {
|
|
|
530
641
|
// the same sentence the approval list carries, counting the rows THIS
|
|
531
642
|
// list has (the alternatives plus the way back)
|
|
532
643
|
if (phase === "safer" && safer !== undefined) {
|
|
533
|
-
return `↑↓ move · ⏎ or click confirms · 1
|
|
644
|
+
return `↑↓ move · ⏎ or click confirms · 1–${safer.options.length + 1} instant · esc`;
|
|
534
645
|
}
|
|
535
|
-
|
|
646
|
+
// Graphite P4 (owner, 2026-10-04): on an approval esc is a denial (the CLI
|
|
647
|
+
// records it as one), so the key row says so; kiso's own questions keep
|
|
648
|
+
// the bare `esc`, which declines them
|
|
649
|
+
return `↑↓ move · ⏎ or click confirms · 1–${panelOptions(view).length} instant · ${view.flavor === "approval" ? "esc denies" : "esc"}`;
|
|
536
650
|
}
|
|
537
|
-
// ── TUI2-R2
|
|
651
|
+
// ── TUI2-R2 ④, Graphite P3: the pick block, its lead, its status ────
|
|
538
652
|
/**
|
|
539
|
-
*
|
|
653
|
+
* Graphite P3 (owner, 2026-10-04) — the pick block takes the shape of every
|
|
654
|
+
* list band (§8.2, §8.13). The band names itself with its count or its
|
|
655
|
+
* current value; a window of eight rows from a 30-row terminal, five below,
|
|
656
|
+
* with dim more-marks in column 0; the rows a TABLE, each column measured
|
|
657
|
+
* over the whole list; the selected row the card head every list shares,
|
|
658
|
+
* OPENED into a second row on the same wash (the /resume shape) for the
|
|
659
|
+
* level strip and what the columns cannot hold; one key row with the
|
|
660
|
+
* counter. No closing rule: the composer's rail closes the band, as it does
|
|
661
|
+
* the command list.
|
|
540
662
|
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* someone remembered to configure is a smaller product than the one it
|
|
547
|
-
* replaced.
|
|
663
|
+
* DECLARED REVERSALS (P3, owner, 2026-10-04): the `current:` row (its words
|
|
664
|
+
* ride the band's name), the bracketed level (the level in force is gold),
|
|
665
|
+
* the `↕ 1-9 / 11` row (the more-marks say it), PICK_MAX's nine-row window
|
|
666
|
+
* (§8.2's eight and five), and the `t` row with its typing phase (a typed
|
|
667
|
+
* `provider/model` is a row of the filter — PickSpec.direct).
|
|
548
668
|
*
|
|
549
|
-
* Single-row discipline: every row CUTS, never folds — the block's
|
|
550
|
-
*
|
|
669
|
+
* Single-row discipline: every row CUTS, never folds — the block's height
|
|
670
|
+
* is its row count (the W20 rule the #checked throw demands).
|
|
551
671
|
*/
|
|
552
|
-
export function pickBlockRows(view, state, W, maxRows) {
|
|
672
|
+
export function pickBlockRows(view, state, W, maxRows, height = 24) {
|
|
553
673
|
const p = palette();
|
|
554
674
|
const spec = view.pick;
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
675
|
+
const list = pickList(spec, state);
|
|
676
|
+
const total = spec.options.length;
|
|
677
|
+
// a legacy `name — words` header: the name opens the band, the words join its facts
|
|
678
|
+
const name = spec.header.split(" — ")[0] ?? spec.header;
|
|
679
|
+
const said = spec.header.slice(name.length).replace(/^ — /, "");
|
|
680
|
+
const matched = list.shown.filter((i) => i < total).length;
|
|
681
|
+
const count = spec.noun === undefined ? "" : list.query !== "" ? `${matched} of ${total} match` : spec.noun === "" ? `${total}` : `${total} ${spec.noun}`;
|
|
682
|
+
const facts = [count, said, spec.facts ?? ""].filter((f) => f !== "").join(" · ");
|
|
683
|
+
const rows = [bandHeader(escapeTerminal(facts === "" ? name : `${name} · ${facts}`), W)];
|
|
684
|
+
const room = Math.max(1, W - 3); // the two-cell lead and one cell of margin
|
|
685
|
+
// the honest empty state — the caller's own copy, verbatim; a typed
|
|
686
|
+
// `provider/model` is still a row under it
|
|
687
|
+
if (total === 0)
|
|
688
|
+
rows.push(` ${cutLine(`${p.dim}${escapeTerminal(spec.emptyNote ?? "no options")}${p.reset}`, room)}`);
|
|
689
|
+
else if (list.shown.length === 0)
|
|
690
|
+
rows.push(` ${cutLine(`${p.dim}nothing matches "${escapeTerminal(list.query)}"${p.reset}`, room)}`);
|
|
691
|
+
if (list.shown.length === 0) {
|
|
692
|
+
// the last sweep (owner, 2026-10-06): a list with nothing in it has no
|
|
693
|
+
// place to count (it read `0/0` under "no profiles"); a filter that
|
|
694
|
+
// matches nothing still counts 0/0, as /resume's does
|
|
695
|
+
rows.push(total === 0 ? cutLine(`${p.dim} esc${p.reset}`, W) : bandKeyRow(["esc"], 0, 0, W));
|
|
696
|
+
return rows;
|
|
561
697
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// REVIEW of this round: the SIZE depends on the frame's budget, so the
|
|
585
|
-
// renderer and the digit keys used to disagree on a short terminal
|
|
586
|
-
// (drawn rows 1-2, keys computing against nine). `pickWindowOf` is the
|
|
587
|
-
// one derivation both sides use — the renderer draws it, the input layer
|
|
588
|
-
// is HANDED it (`visiblePickWindow`).
|
|
589
|
-
const win = pickWindowOf(view, state.cursor, state.phase, maxRows);
|
|
590
|
-
const shown = spec.options.slice(win.first, win.first + win.size);
|
|
591
|
-
// R2: the note takes a COLUMN, not three spaces after a label of
|
|
592
|
-
// whatever length this row happened to have, and the cursor row
|
|
593
|
-
// wears the bar and the arrow like every other list in the
|
|
594
|
-
// product. This panel was the last one still saying "selected"
|
|
595
|
-
// with bold alone.
|
|
596
|
-
// 0.40.1 (the owner's dogfood, 2026-09-21): the row NUMBER is gone. The
|
|
597
|
-
// digits still pick by visible position (the keys sheet documents them),
|
|
598
|
-
// but a number the person reads as decoration is noise the label pays
|
|
599
|
-
// for at every width — and the owner picks with ↑↓ and the mouse.
|
|
600
|
-
const lead = (o, _i, cursor) => `${cursor ? "\u2192" : " "} ${escapeTerminal(o.label)}`;
|
|
601
|
-
const widest = Math.max(...shown.map((o, i) => visibleWidth(lead(o, i, false))));
|
|
602
|
-
const stop = shown.some((o) => o.note !== undefined) && widest + 2 <= Math.floor(room / 2) && room - widest - 2 >= 18 ? widest + 2 : 0;
|
|
603
|
-
for (let i = 0; i < shown.length; i += 1) {
|
|
604
|
-
const o = shown[i];
|
|
605
|
-
// DC-58: `i` is the row's position in the WINDOW, the cursor is an
|
|
606
|
-
// option index — the mark lands on the cursor's own row after any
|
|
607
|
-
// scroll only when the window's origin is added back.
|
|
608
|
-
const mark = win.first + i === state.cursor && state.phase === "options";
|
|
609
|
-
const plain = lead(o, i, mark);
|
|
610
|
-
const head = `${mark ? p.bold : ""}${plain}${mark ? p.reset : ""}`;
|
|
611
|
-
const note = o.note === undefined
|
|
612
|
-
? ""
|
|
613
|
-
: stop > 0
|
|
614
|
-
? `${" ".repeat(Math.max(1, stop - visibleWidth(plain)))}${p.dim}${widthCut(escapeTerminal(o.note), Math.max(0, room - stop))}${p.reset}`
|
|
615
|
-
: `${p.dim} ${escapeTerminal(o.note)}${p.reset}`;
|
|
616
|
-
const text = cutLine(`${head}${note}`, room);
|
|
617
|
-
// ONE space, like the approval and ask panels: the bar spends a
|
|
618
|
-
// leading cell of its own, so a two-space unselected prefix
|
|
619
|
-
// moves the digit column by one as the cursor walks — the exact
|
|
620
|
-
// "a column that moves per row reads as damage" this file
|
|
621
|
-
// quotes twice as its standard.
|
|
622
|
-
rows.push(mark ? selectionBar(text, visibleWidth(text), W) : ` ${text}`);
|
|
623
|
-
}
|
|
624
|
-
if (spec.options.length > shown.length) {
|
|
625
|
-
const to = win.first + shown.length;
|
|
626
|
-
rows.push(` ${cutLine(`${p.dim} \u2195 ${win.first + 1}-${to} / ${spec.options.length} \u2014 \u2191\u2193 scrolls${p.reset}`, room)}`);
|
|
698
|
+
const win = pickWindowOf(view, state, maxRows, height);
|
|
699
|
+
const colsOf = (o) => (o.cols ?? (o.note === undefined ? [] : [o.note])).map(escapeTerminal);
|
|
700
|
+
// the columns measure over EVERY option, so a filter or a walked level
|
|
701
|
+
// never moves them
|
|
702
|
+
const labelW = Math.max(0, ...spec.options.map((o) => visibleWidth(escapeTerminal(o.label))));
|
|
703
|
+
const colW = [];
|
|
704
|
+
for (const o of spec.options) {
|
|
705
|
+
colsOf(o).forEach((c, j) => {
|
|
706
|
+
const levels = j === spec.levelColumn ? (o.levels ?? []).map((l) => visibleWidth(escapeTerminal(l))) : [];
|
|
707
|
+
colW[j] = Math.max(colW[j] ?? 0, visibleWidth(c), ...levels);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const lead = (mark) => (mark === null ? " " : `${p.dim}${mark}${p.reset}`);
|
|
711
|
+
for (let k = win.first; k < win.first + win.size; k += 1) {
|
|
712
|
+
const i = list.shown[k];
|
|
713
|
+
const on = i === list.cursor;
|
|
714
|
+
const mark = moreMark(k, win.first, win.size, list.shown.length);
|
|
715
|
+
if (i === total) {
|
|
716
|
+
// the direct row: what was typed, offered as a pick of its own
|
|
717
|
+
const text = cutLine(`use ${escapeTerminal(list.direct ?? "")} directly`, room);
|
|
718
|
+
rows.push(on ? selectionBar(`${p.bold}→${text}${p.reset}`, visibleWidth(text) + 1, W) : `${lead(mark)} ${p.dim}${text}${p.reset}`);
|
|
719
|
+
continue;
|
|
627
720
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
//
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const note = axis.levelNote === undefined ? "" : ` ${p.dim}\u2014 ${escapeTerminal(axis.levelNote)}${p.reset}`;
|
|
648
|
-
rows.push(` ${cutLine(`${p.dim}effort: ${p.reset}${cells.join(`${p.dim} \u00b7 ${p.reset}`)}${note}`, room)}`);
|
|
721
|
+
const o = spec.options[i];
|
|
722
|
+
const hit = list.hits.get(i);
|
|
723
|
+
const label = escapeTerminal(o.label);
|
|
724
|
+
let text = `${goldHits(label, new Set(hit?.col === 0 ? hit.at : []), on ? p.bold : o.off === true ? p.dim : "")}${" ".repeat(Math.max(0, labelW - visibleWidth(label)))}`;
|
|
725
|
+
let width = labelW;
|
|
726
|
+
const cols = colsOf(o);
|
|
727
|
+
cols.forEach((c, j) => {
|
|
728
|
+
// a setting's value follows ←→ on the selected row
|
|
729
|
+
const shown = on && j === spec.levelColumn && o.levels !== undefined && list.level !== null ? escapeTerminal(o.levels[list.level] ?? c) : c;
|
|
730
|
+
const plain = spec.columns?.[j] === "plain";
|
|
731
|
+
const style = plain ? (on ? p.bold : o.off === true ? p.dim : "") : p.dim;
|
|
732
|
+
const last = j === cols.length - 1;
|
|
733
|
+
text += ` ${goldHits(shown, new Set(hit?.col === j + 1 ? hit.at : []), style)}${last ? "" : " ".repeat(Math.max(0, (colW[j] ?? 0) - visibleWidth(shown)))}`;
|
|
734
|
+
width += 2 + (last ? visibleWidth(shown) : (colW[j] ?? 0));
|
|
735
|
+
});
|
|
736
|
+
const cut = width > room ? cutLine(text, room) : text;
|
|
737
|
+
if (!on) {
|
|
738
|
+
rows.push(`${lead(mark)} ${cut}`);
|
|
739
|
+
continue;
|
|
649
740
|
}
|
|
741
|
+
rows.push(selectionBar(`${p.bold}→${cut}`, Math.min(width, room) + 1, W));
|
|
742
|
+
const open = openedRow(o, list.level, room);
|
|
743
|
+
if (open !== null)
|
|
744
|
+
rows.push(selectionBar(` ${open.text}`, open.width + 1, W));
|
|
650
745
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
746
|
+
const here = list.cursor >= 0 && list.cursor < total ? spec.options[list.cursor] : undefined;
|
|
747
|
+
const keys = ["↑↓ move"];
|
|
748
|
+
if (here?.levels !== undefined && here.levels.length > 0)
|
|
749
|
+
keys.push(`←→ ${here.axisLabel ?? "effort"}`);
|
|
750
|
+
if ((spec.input ?? "digits") === "digits" && win.size > 1)
|
|
751
|
+
keys.push(`1–${Math.min(win.size, PICK_MAX)} picks`);
|
|
752
|
+
keys.push(here?.enter ?? spec.enter ?? "⏎ confirms", "esc");
|
|
753
|
+
rows.push(bandKeyRow(keys, Math.max(0, list.shown.indexOf(list.cursor)), list.shown.length, W));
|
|
658
754
|
return rows;
|
|
659
755
|
}
|
|
660
|
-
/**
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
756
|
+
/** Text cut to `room` cells by cells, with an ellipsis — plain text only. */
|
|
757
|
+
const fitCells = (t, room) => (visibleWidth(t) <= room ? t : room <= 1 ? "" : `${widthCut(t, room - 1)}…`);
|
|
758
|
+
/** The selected row's second row: the level strip (the level in force
|
|
759
|
+
* bold gold, a level the thinking mode forbids dim), then what the caller
|
|
760
|
+
* has to say — the strip stays whole and the words give way. Null when
|
|
761
|
+
* there is nothing to open into. */
|
|
762
|
+
function openedRow(o, level, room) {
|
|
763
|
+
const p = palette();
|
|
764
|
+
// off a known ground the warn tint carries the gold, as the typed letters do
|
|
765
|
+
const gold = p.gold !== "" ? p.gold : p.warn;
|
|
766
|
+
const tail = o.levelNote ?? o.opened ?? "";
|
|
767
|
+
if (o.levels === undefined || o.levels.length === 0) {
|
|
768
|
+
if (tail === "")
|
|
769
|
+
return null;
|
|
770
|
+
const t = fitCells(escapeTerminal(tail), room);
|
|
771
|
+
return { text: `${p.dim}${t}${p.reset}`, width: visibleWidth(t) };
|
|
772
|
+
}
|
|
773
|
+
const off = new Set(o.disabled ?? []);
|
|
774
|
+
// a named axis is the row's own value (a setting), so its strip needs no
|
|
775
|
+
// label; the model's effort is not the row's label, so it says `effort`
|
|
776
|
+
const label = o.axisLabel === undefined ? "effort " : "";
|
|
777
|
+
const levels = o.levels.map((l) => escapeTerminal(l));
|
|
778
|
+
const cells = levels.map((t, i) => (i === level ? `${p.bold}${gold}${t}${p.reset}` : off.has(i) ? `${p.dim}${t}${p.reset}` : t));
|
|
779
|
+
let text = `${label === "" ? "" : `${p.dim}${label}${p.reset}`}${cells.join(`${p.dim} · ${p.reset}`)}`;
|
|
780
|
+
let width = visibleWidth(`${label}${levels.join(" · ")}`);
|
|
781
|
+
const t = tail === "" ? "" : fitCells(escapeTerminal(tail), room - width - 5);
|
|
782
|
+
if (t !== "") {
|
|
783
|
+
text += `${p.dim} · ${t}${p.reset}`;
|
|
784
|
+
width += 5 + visibleWidth(t);
|
|
785
|
+
}
|
|
786
|
+
return width > room ? { text: cutLine(text, room), width: room } : { text, width };
|
|
787
|
+
}
|
|
788
|
+
/** The digits are the keys, so what one SCREEN offers is bounded by the
|
|
789
|
+
* digits there are. Finding DC-58 (the owner, 2026-09-21) made the window
|
|
790
|
+
* follow the cursor, so this is how many rows a digit can name, not how
|
|
791
|
+
* many rows the keyboard reaches. */
|
|
669
792
|
export const PICK_MAX = 9;
|
|
670
|
-
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* rather than short.
|
|
675
|
-
*
|
|
676
|
-
* Pure, and the ONE copy: the renderer, the digit keys and the click
|
|
677
|
-
* hit-test all ask this function rather than each deriving an offset.
|
|
678
|
-
*/
|
|
679
|
-
/** The pick panel's window for THIS frame's budget — the one derivation the
|
|
680
|
-
* renderer and the input layer share (review of this round).
|
|
793
|
+
/**
|
|
794
|
+
* The pick's window for THIS frame — the one derivation the renderer and
|
|
795
|
+
* the digit keys share (DC-58's review: a size derived twice is a size that
|
|
796
|
+
* can disagree). Positions are into `pickList(...).shown`.
|
|
681
797
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* keeps its zero-dependency contract.
|
|
798
|
+
* Graphite P3: §8.2's window (eight rows from a 30-row terminal, five
|
|
799
|
+
* below, the cursor kept a row inside an edge while more lies past it),
|
|
800
|
+
* within the frame's budget: the band's name, the key row, the opened row
|
|
801
|
+
* and a `nothing matches` row are the chrome.
|
|
687
802
|
*/
|
|
688
|
-
export function pickWindowOf(view,
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return { first, size: win };
|
|
701
|
-
}
|
|
702
|
-
/** The input row's lead: the digit range while picking, the named
|
|
703
|
-
* prompt while typing one out. */
|
|
704
|
-
export function pickLeadPlain(view, state) {
|
|
705
|
-
if (state.phase === "custom")
|
|
706
|
-
return "provider/model: ";
|
|
707
|
-
const n = Math.min(view.pick.options.length, PICK_MAX);
|
|
708
|
-
return n === 0 ? "t> " : `1-${n}> `;
|
|
709
|
-
}
|
|
710
|
-
export function pickLead(view, state) {
|
|
711
|
-
const p = palette();
|
|
712
|
-
return `${p.bold}${pickLeadPlain(view, state)}${p.reset}`;
|
|
803
|
+
export function pickWindowOf(view, state, maxRows, height = 24) {
|
|
804
|
+
const spec = view.pick;
|
|
805
|
+
if (spec === undefined)
|
|
806
|
+
return { first: 0, size: 0 };
|
|
807
|
+
const list = pickList(spec, state);
|
|
808
|
+
const here = list.cursor >= 0 && list.cursor < spec.options.length ? spec.options[list.cursor] : undefined;
|
|
809
|
+
const opens = here !== undefined && ((here.levels?.length ?? 0) > 0 || (here.levelNote ?? here.opened ?? "") !== "") ? 1 : 0;
|
|
810
|
+
const notice = spec.options.length === 0 || list.shown.length === 0 ? 1 : 0;
|
|
811
|
+
const chrome = 2 + opens + notice;
|
|
812
|
+
const size = Math.max(1, Math.min(bandVisible(height), maxRows - chrome));
|
|
813
|
+
const { first, count } = bandWindow(list.shown.length, Math.max(0, list.shown.indexOf(list.cursor)), size);
|
|
814
|
+
return { first, size: count };
|
|
713
815
|
}
|
|
714
|
-
/** The
|
|
715
|
-
*
|
|
816
|
+
/** The input row's lead. Graphite P3: a pick leaves the input row to the
|
|
817
|
+
* composer — the filter is typed there, and a digit list needs no lead
|
|
818
|
+
* to say so (its key row does). */
|
|
819
|
+
export function pickLeadPlain(_view, _state) {
|
|
820
|
+
return "";
|
|
821
|
+
}
|
|
822
|
+
export function pickLead(_view, _state) {
|
|
823
|
+
return "";
|
|
824
|
+
}
|
|
825
|
+
/** The status row's left text — the CALLER's, because only the caller
|
|
826
|
+
* knows whether a run is paused behind this panel. (Graphite P3: on a
|
|
827
|
+
* dock the bar stays under a pick; this is the dock-less text.) */
|
|
716
828
|
export function pickStatus(view) {
|
|
717
829
|
return view.statusText;
|
|
718
830
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
// that omits the gesture is why the gesture goes unused.
|
|
726
|
-
// OR-7 names \u2190\u2192 here for the same reason DC-36 named \u2191\u2193: a gesture
|
|
727
|
-
// this row omits is a gesture that goes unused. It appears only when
|
|
728
|
-
// there is a second axis to walk.
|
|
729
|
-
if (state.phase === "custom")
|
|
730
|
-
return "enter commits \u00b7 esc backs out";
|
|
731
|
-
return axis ? "\u2191\u2193 move \u00b7 \u2190\u2192 effort \u00b7 digits pick \u00b7 \u23ce confirms \u00b7 esc" : "\u2191\u2193 move \u00b7 digits pick \u00b7 \u23ce confirms \u00b7 esc";
|
|
831
|
+
/** The key words, for a caller that draws them outside the block. The
|
|
832
|
+
* block's own key row is the one on screen (P3). */
|
|
833
|
+
export function pickAffordance(_state, axis = false) {
|
|
834
|
+
// DC-36 — the row NAMES the arrows; OR-7 names ←→ for the same reason,
|
|
835
|
+
// and only when there is a second axis to walk
|
|
836
|
+
return axis !== false ? `↑↓ move · ←→ ${axis === true ? "effort" : axis} · ⏎ confirms · esc` : "↑↓ move · ⏎ confirms · esc";
|
|
732
837
|
}
|
|
733
838
|
/** Compose a pick view. The flavor/name/title/args fields exist for the
|
|
734
839
|
* approval path and are given inert values here \u2014 the pick block
|
|
@@ -752,6 +857,20 @@ export function modePickView(spec, statusText) {
|
|
|
752
857
|
pick: spec,
|
|
753
858
|
};
|
|
754
859
|
}
|
|
860
|
+
/** Graphite R3e — `/settings` as a pick panel: a row per setting, the
|
|
861
|
+
* session's own ones with a second axis to walk. */
|
|
862
|
+
export function settingsPickView(spec, statusText) {
|
|
863
|
+
return {
|
|
864
|
+
flavor: "simple",
|
|
865
|
+
name: "settings",
|
|
866
|
+
title: "settings",
|
|
867
|
+
speaker: "you",
|
|
868
|
+
statusText,
|
|
869
|
+
args: { kind: "text", lines: [] },
|
|
870
|
+
fallbackQuestion: "settings: (name) ",
|
|
871
|
+
pick: spec,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
755
874
|
/** ADR-0058 (3e): the `/tasks` list, a task's actions, and the exit
|
|
756
875
|
* question — the same pick panel as /mode and /model, under its name. */
|
|
757
876
|
export function namedPickView(name, spec, statusText, fallbackQuestion) {
|