@vincemakes/kiso-tui 0.9.0 → 0.10.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/ask-panel.d.ts +8 -7
- package/dist/ask-panel.js +22 -11
- package/dist/compositor.d.ts +8 -9
- package/dist/compositor.js +185 -28
- package/dist/editor.d.ts +8 -1
- package/dist/editor.js +266 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -3
- package/dist/session-picker.d.ts +113 -0
- package/dist/session-picker.js +249 -0
- package/package.json +2 -2
package/dist/editor.js
CHANGED
|
@@ -26,10 +26,15 @@ import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
|
26
26
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
27
27
|
export { charWidth, displayWidth, widthOf };
|
|
28
28
|
import { palette } from "./render.js";
|
|
29
|
+
import { PICK_MAX } from "./approval-panel.js";
|
|
29
30
|
// KC3.5: the panel-slot dispatchers — the ask branch folded into the
|
|
30
31
|
// W21 lead/rows, so this file keeps ONE panel and one key owner.
|
|
31
32
|
import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
|
|
32
33
|
import { AT_VISIBLE, atFilter } from "./at-picker.js";
|
|
34
|
+
// TUI2-R2 ②: the session picker — the band's THIRD occupant. Its filter
|
|
35
|
+
// is the @ picker's rank aimed at the session id; the editor owns the
|
|
36
|
+
// keys, the compositor draws the rows.
|
|
37
|
+
import { sessionFilter } from "./session-picker.js";
|
|
33
38
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
34
39
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
35
40
|
// prompt keeps its own "you> " — v2a line mode, byte-for-byte).
|
|
@@ -135,6 +140,20 @@ export class Editor {
|
|
|
135
140
|
// next open re-snapshots, so a stale list can never be shown.
|
|
136
141
|
#atList = null;
|
|
137
142
|
#atItems = null;
|
|
143
|
+
// TUI2-R2 ② — the session picker. Two fields: the bound source (its
|
|
144
|
+
// presence IS "the picker is up") and the selection. The query, like
|
|
145
|
+
// the @ picker's, is DERIVED from the buffer on every read rather
|
|
146
|
+
// than stored — so every existing buffer op (backspace, the kills,
|
|
147
|
+
// paste) filters correctly with no handler of its own.
|
|
148
|
+
//
|
|
149
|
+
// The picker is MODAL in a way the @ picker is not: it opens before
|
|
150
|
+
// a session exists, owns the whole composer, and the only ways out
|
|
151
|
+
// are a pick and an esc. That is why the commit callback lives here
|
|
152
|
+
// rather than on the line channel — the caller is waiting for an id,
|
|
153
|
+
// not for a turn.
|
|
154
|
+
#pickCards = null;
|
|
155
|
+
#pickCommit = null;
|
|
156
|
+
#pickSel = 0;
|
|
138
157
|
// A2 (the feel): the session-scoped input history — every submitted TURN
|
|
139
158
|
// line (never a question answer), capped at 100, never persisted. ↑↓
|
|
140
159
|
// navigate it ONLY from an empty input or while already browsing.
|
|
@@ -254,7 +273,7 @@ export class Editor {
|
|
|
254
273
|
* the frame's clamp is the authority. */
|
|
255
274
|
#visibleRows(lineCount) {
|
|
256
275
|
const H = process.stdout.rows ?? 24;
|
|
257
|
-
const bands = (this.#menuOpen ? this.#menuFiltered().length : 0) + this.#atRows() + this.#queueState().length;
|
|
276
|
+
const bands = (this.#menuOpen ? this.#menuFiltered().length : 0) + this.#atRows() + this.#pickRows() + this.#queueState().length;
|
|
258
277
|
return Math.max(1, Math.min(lineCount, N_MAX, Math.max(1, H - 3 - bands)));
|
|
259
278
|
}
|
|
260
279
|
/** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
|
|
@@ -392,6 +411,11 @@ export class Editor {
|
|
|
392
411
|
#atArm() {
|
|
393
412
|
if (this.#atItems === null)
|
|
394
413
|
return;
|
|
414
|
+
// TUI2-R2 ②: not inside a session filter. An `@` typed into the
|
|
415
|
+
// picker's query is a character in a session id, and a file picker
|
|
416
|
+
// opening over a session picker would put two bands in one slot.
|
|
417
|
+
if (this.#pickUp())
|
|
418
|
+
return;
|
|
395
419
|
if (this.#panel !== null || this.#menuOpen || this.#questionCb !== null)
|
|
396
420
|
return;
|
|
397
421
|
if (this.#atToken() === null)
|
|
@@ -437,6 +461,71 @@ export class Editor {
|
|
|
437
461
|
const view = this.#atView();
|
|
438
462
|
return view === null ? 0 : Math.min(view.matches.length, AT_VISIBLE) + 1;
|
|
439
463
|
}
|
|
464
|
+
// ── TUI2-R2 ② — the session picker ───────────────────────────────
|
|
465
|
+
/** Open the picker on a bound card source. The composer is cleared
|
|
466
|
+
* (the buffer becomes the filter query) and `onPick` receives the
|
|
467
|
+
* chosen id — or null when the human leaves without picking, which
|
|
468
|
+
* is a first-class outcome and not an error. */
|
|
469
|
+
beginPick(cards, onPick) {
|
|
470
|
+
this.#pickCards = cards;
|
|
471
|
+
this.#pickCommit = onPick;
|
|
472
|
+
this.#pickSel = 0;
|
|
473
|
+
this.#chars = [];
|
|
474
|
+
this.#cursor = 0;
|
|
475
|
+
this.#reflow();
|
|
476
|
+
this.#onRender();
|
|
477
|
+
}
|
|
478
|
+
/** The picker's state, derived: the full card list (the id column
|
|
479
|
+
* measures over ALL of them, so the columns never jump), the
|
|
480
|
+
* filtered matches, and the selection CLAMPED at read time — the
|
|
481
|
+
* same correction discipline the @ picker uses, for the same
|
|
482
|
+
* reason: narrowing can only ever shrink the list. */
|
|
483
|
+
#pickView() {
|
|
484
|
+
if (this.#pickCards === null)
|
|
485
|
+
return null;
|
|
486
|
+
const cards = this.#pickCards();
|
|
487
|
+
const matches = sessionFilter(cards, this.line());
|
|
488
|
+
return { cards, matches, selected: Math.max(0, Math.min(this.#pickSel, matches.length - 1)) };
|
|
489
|
+
}
|
|
490
|
+
pickState() {
|
|
491
|
+
return this.#pickView();
|
|
492
|
+
}
|
|
493
|
+
#pickUp() {
|
|
494
|
+
return this.#pickCards !== null;
|
|
495
|
+
}
|
|
496
|
+
/** The band's height estimate: the header + the windowed rows (or
|
|
497
|
+
* the one "no match" row) + the counter. */
|
|
498
|
+
#pickRows() {
|
|
499
|
+
const view = this.#pickView();
|
|
500
|
+
return view === null ? 0 : Math.min(Math.max(view.matches.length, 1), AT_VISIBLE) + 2;
|
|
501
|
+
}
|
|
502
|
+
/** Close and hand the verdict back. The callback fires AFTER the
|
|
503
|
+
* state is cleared, so a caller that re-enters (a second picker, a
|
|
504
|
+
* session that starts) never sees the closing picker's rows. */
|
|
505
|
+
#pickClose(id) {
|
|
506
|
+
const cb = this.#pickCommit;
|
|
507
|
+
this.#pickCards = null;
|
|
508
|
+
this.#pickCommit = null;
|
|
509
|
+
this.#pickSel = 0;
|
|
510
|
+
this.#chars = [];
|
|
511
|
+
this.#cursor = 0;
|
|
512
|
+
this.#reflow();
|
|
513
|
+
cb?.(id);
|
|
514
|
+
this.#onRender();
|
|
515
|
+
}
|
|
516
|
+
/** Enter takes the SELECTED session. An empty match set takes
|
|
517
|
+
* nothing and leaves the picker up: a picker that invented a pick
|
|
518
|
+
* when the query matched nothing would resume the wrong session,
|
|
519
|
+
* which is the one failure this surface must never have. */
|
|
520
|
+
#pickAccept() {
|
|
521
|
+
const view = this.#pickView();
|
|
522
|
+
if (view === null)
|
|
523
|
+
return;
|
|
524
|
+
const card = view.matches[view.selected];
|
|
525
|
+
if (card === undefined)
|
|
526
|
+
return;
|
|
527
|
+
this.#pickClose(card.id);
|
|
528
|
+
}
|
|
440
529
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
441
530
|
question(_query, cb) {
|
|
442
531
|
this.#questionCb = cb;
|
|
@@ -455,6 +544,9 @@ export class Editor {
|
|
|
455
544
|
phase: "options",
|
|
456
545
|
sel: 0,
|
|
457
546
|
ask: view.ask === undefined ? null : askStart(view.ask),
|
|
547
|
+
// TUI2-R2 ④: the pick's walk — present exactly when the view is
|
|
548
|
+
// a pick, the same contract the ask's runtime has.
|
|
549
|
+
pick: view.pick === undefined ? null : { cursor: 0, phase: "options" },
|
|
458
550
|
amend: "yes",
|
|
459
551
|
onCommit,
|
|
460
552
|
stash: { chars: this.#chars, cursor: this.#cursor, scroll: this.#scroll },
|
|
@@ -479,7 +571,13 @@ export class Editor {
|
|
|
479
571
|
const panel = this.#panel;
|
|
480
572
|
if (panel === null)
|
|
481
573
|
return null;
|
|
482
|
-
return {
|
|
574
|
+
return {
|
|
575
|
+
view: panel.view,
|
|
576
|
+
phase: panel.phase,
|
|
577
|
+
sel: panel.sel,
|
|
578
|
+
...(panel.ask === null ? {} : { ask: panel.ask }),
|
|
579
|
+
...(panel.pick === null ? {} : { pick: panel.pick }),
|
|
580
|
+
};
|
|
483
581
|
}
|
|
484
582
|
enter() {
|
|
485
583
|
if (this.#entered)
|
|
@@ -554,6 +652,44 @@ export class Editor {
|
|
|
554
652
|
// only esc and enter are intercepted while typing), esc
|
|
555
653
|
// declines the whole call. Everything else falls through to
|
|
556
654
|
// the ordinary editing chain below.
|
|
655
|
+
// TUI2-R2 ④: a PICK panel routes its own keys — a digit moves
|
|
656
|
+
// the cursor to that option (never commits: the choice is
|
|
657
|
+
// CONFIRMED, so a mistyped digit is a mistake you can see
|
|
658
|
+
// before it takes effect), `t` opens the type-it line, enter
|
|
659
|
+
// commits, esc backs out then cancels. The swallow rule below
|
|
660
|
+
// is the ask's, for the ask's reason: a typed `/` must not arm
|
|
661
|
+
// the menu under a panel that owns the keys.
|
|
662
|
+
if (panel.pick !== null) {
|
|
663
|
+
const typing = panel.pick.phase === "custom";
|
|
664
|
+
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
665
|
+
this.#pickPanelEsc();
|
|
666
|
+
i += 1;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (c === "\x0d" || c === "\x0a") {
|
|
670
|
+
this.#pickPanelEnter();
|
|
671
|
+
i += 1;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (!typing && c !== undefined && c >= "1" && c <= "9") {
|
|
675
|
+
this.#pickPanelDigit(Number(c) - 1);
|
|
676
|
+
i += 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (!typing && (c === "t" || c === "T")) {
|
|
680
|
+
panel.pick = { cursor: panel.pick.cursor, phase: "custom" };
|
|
681
|
+
this.#chars = [];
|
|
682
|
+
this.#cursor = 0;
|
|
683
|
+
this.#scroll = 0;
|
|
684
|
+
this.#onRender();
|
|
685
|
+
i += 1;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (!typing && c !== undefined && c >= " " && c !== "\x7f") {
|
|
689
|
+
i += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
557
693
|
if (panel.ask !== null) {
|
|
558
694
|
const typing = panel.ask.phase === "custom";
|
|
559
695
|
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
@@ -602,22 +738,49 @@ export class Editor {
|
|
|
602
738
|
i += 1;
|
|
603
739
|
continue;
|
|
604
740
|
}
|
|
605
|
-
|
|
606
|
-
|
|
741
|
+
// TUI2-R2 ⑧ — the shortcut keys belong to the OPTIONS phase and
|
|
742
|
+
// to it alone.
|
|
743
|
+
//
|
|
744
|
+
// `1`/`y` select yes and `3`/`n` select no, and they used to be
|
|
745
|
+
// applied in every phase of every flavour: the `i += 1;
|
|
746
|
+
// continue;` sat OUTSIDE the phase check, so a phase where the
|
|
747
|
+
// key meant nothing swallowed it anyway. A phase where a letter
|
|
748
|
+
// means nothing is exactly a phase where a human is typing
|
|
749
|
+
// prose — so every y, n, 1 and 3 vanished from the line,
|
|
750
|
+
// silently, with no error and no visible cause. "yes, run it
|
|
751
|
+
// now 13" committed as "es, ru it ow ".
|
|
752
|
+
//
|
|
753
|
+
// Three typed phases were affected: the ask's custom answer,
|
|
754
|
+
// the approval panel's rule input, and its amend/feedback line.
|
|
755
|
+
// The rule input is the one that mattered most — it writes a
|
|
756
|
+
// DURABLE don't-ask-again rule, so a dropped character persists
|
|
757
|
+
// a rule the human never typed.
|
|
758
|
+
//
|
|
759
|
+
// Slice ④ met the same mechanism on the new pick panel
|
|
760
|
+
// ("openai/deepseek-reasoner" -> "opeai/deepseek-reasoer") and
|
|
761
|
+
// guarded pick alone, because the rest was a behaviour change
|
|
762
|
+
// owed its own red. This is that guard, stated once for every
|
|
763
|
+
// flavour: the options phase keeps its keys, and every typed
|
|
764
|
+
// phase keeps its text.
|
|
765
|
+
const optionsPhase = panel.pick === null && // a pick has no yes and no no
|
|
766
|
+
panel.phase === "options" && // the rule / amend lines are prose
|
|
767
|
+
(panel.ask === null || panel.ask.phase === "options"); // and so is a typed ask answer
|
|
768
|
+
if (optionsPhase) {
|
|
769
|
+
if (c === "1" || c === "y" || c === "Y") {
|
|
607
770
|
this.#panelSelect(1);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (panel.phase === "options")
|
|
771
|
+
i += 1;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (c === "2" && panel.view.flavor === "approval") {
|
|
775
|
+
this.#panelRule();
|
|
776
|
+
i += 1;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (c === "3" || c === "n" || c === "N") {
|
|
618
780
|
this.#panelSelect(3);
|
|
619
|
-
|
|
620
|
-
|
|
781
|
+
i += 1;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
621
784
|
}
|
|
622
785
|
}
|
|
623
786
|
if (c === "\x1b") {
|
|
@@ -656,6 +819,15 @@ export class Editor {
|
|
|
656
819
|
this.#refreshMenu();
|
|
657
820
|
i += 1;
|
|
658
821
|
}
|
|
822
|
+
else if (this.#pickUp()) {
|
|
823
|
+
// TUI2-R2 ②: esc leaves the picker with nothing picked.
|
|
824
|
+
// The caller reads null and exits 0 — declining to resume
|
|
825
|
+
// is a normal thing to do, not a failure, so it must not
|
|
826
|
+
// fall through to the escapeCbs (which mean "abort the
|
|
827
|
+
// run" and there is no run yet).
|
|
828
|
+
this.#pickClose(null);
|
|
829
|
+
i += 1;
|
|
830
|
+
}
|
|
659
831
|
else if (this.#atUp()) {
|
|
660
832
|
// KC3 §3: esc closes the picker and leaves the BUFFER
|
|
661
833
|
// ALONE — unlike the menu's esc, which clears it. The
|
|
@@ -839,9 +1011,25 @@ export class Editor {
|
|
|
839
1011
|
if (this.#panel !== null) {
|
|
840
1012
|
// W21: the panel owns the keys. KC3.5: an ask uses ↑↓ for
|
|
841
1013
|
// the option cursor (the approval panel still has no ↑↓ role).
|
|
842
|
-
if (this.#panel.
|
|
1014
|
+
if (this.#panel.pick !== null && this.#panel.pick.phase === "options") {
|
|
1015
|
+
// TUI2-R2 ④: ↑↓ walk the pick's cursor — the same list the
|
|
1016
|
+
// digits address, the other muscle.
|
|
1017
|
+
const n = Math.min(this.#panel.view.pick.options.length, PICK_MAX);
|
|
1018
|
+
const cur = this.#panel.pick.cursor;
|
|
1019
|
+
this.#panel.pick = { cursor: final === "A" ? Math.max(0, cur - 1) : Math.min(Math.max(0, n - 1), cur + 1), phase: "options" };
|
|
1020
|
+
}
|
|
1021
|
+
else if (this.#panel.ask !== null && this.#panel.ask.phase === "options")
|
|
843
1022
|
this.#askStep(final === "A" ? "up" : "down");
|
|
844
1023
|
}
|
|
1024
|
+
else if (this.#pickUp()) {
|
|
1025
|
+
// TUI2-R2 ②: the session picker owns ↑↓ while up — the
|
|
1026
|
+
// SELECTION, never the composer's line walk and never the
|
|
1027
|
+
// history browse. It sits above both because the picker is
|
|
1028
|
+
// modal: there is no turn to recall and no second line to
|
|
1029
|
+
// walk to while it is open.
|
|
1030
|
+
const view = this.#pickView();
|
|
1031
|
+
this.#pickSel = final === "A" ? Math.max(0, view.selected - 1) : Math.min(Math.max(0, view.matches.length - 1), view.selected + 1);
|
|
1032
|
+
}
|
|
845
1033
|
else if (this.#menuOpen) {
|
|
846
1034
|
if (final === "A")
|
|
847
1035
|
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
@@ -1025,6 +1213,60 @@ export class Editor {
|
|
|
1025
1213
|
}
|
|
1026
1214
|
this.#onRender();
|
|
1027
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* TUI2-R2 ④ — the pick panel's three keys.
|
|
1218
|
+
*
|
|
1219
|
+
* A digit MOVES the cursor rather than committing. The list is short
|
|
1220
|
+
* and the digits are adjacent on the keyboard; a picker that acted on
|
|
1221
|
+
* the keypress would make a mistyped 3 a model switch, and the whole
|
|
1222
|
+
* point of a confirm step is that the choice is visible before it is
|
|
1223
|
+
* taken.
|
|
1224
|
+
*/
|
|
1225
|
+
#pickPanelDigit(index) {
|
|
1226
|
+
const panel = this.#panel;
|
|
1227
|
+
if (panel === null || panel.pick === null)
|
|
1228
|
+
return;
|
|
1229
|
+
// a digit past the list is INERT — an option nobody has is never
|
|
1230
|
+
// selected, and the cursor stays where the human left it
|
|
1231
|
+
if (index < 0 || index >= Math.min(panel.view.pick.options.length, PICK_MAX))
|
|
1232
|
+
return;
|
|
1233
|
+
panel.pick = { cursor: index, phase: "options" };
|
|
1234
|
+
this.#onRender();
|
|
1235
|
+
}
|
|
1236
|
+
/** enter — the typed line when there is one (an EMPTY line is not a
|
|
1237
|
+
* choice and commits nothing), else the option under the cursor. */
|
|
1238
|
+
#pickPanelEnter() {
|
|
1239
|
+
const panel = this.#panel;
|
|
1240
|
+
if (panel === null || panel.pick === null)
|
|
1241
|
+
return;
|
|
1242
|
+
if (panel.pick.phase === "custom") {
|
|
1243
|
+
const line = this.line().trim();
|
|
1244
|
+
if (line === "")
|
|
1245
|
+
return;
|
|
1246
|
+
this.#panelClose({ action: "picked", result: { custom: line } });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (panel.view.pick.options.length === 0)
|
|
1250
|
+
return; // nothing to take
|
|
1251
|
+
this.#panelClose({ action: "picked", result: { index: panel.pick.cursor } });
|
|
1252
|
+
}
|
|
1253
|
+
/** esc — back out of the type-it line first, then cancel the panel.
|
|
1254
|
+
* Two escapes, two meanings, exactly as the approval panel's
|
|
1255
|
+
* rule/amend phases already work. */
|
|
1256
|
+
#pickPanelEsc() {
|
|
1257
|
+
const panel = this.#panel;
|
|
1258
|
+
if (panel === null || panel.pick === null)
|
|
1259
|
+
return;
|
|
1260
|
+
if (panel.pick.phase === "custom") {
|
|
1261
|
+
panel.pick = { cursor: panel.pick.cursor, phase: "options" };
|
|
1262
|
+
this.#chars = [];
|
|
1263
|
+
this.#cursor = 0;
|
|
1264
|
+
this.#scroll = 0;
|
|
1265
|
+
this.#onRender();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.#panelClose({ action: "cancel" });
|
|
1269
|
+
}
|
|
1028
1270
|
#panelClose(verdict) {
|
|
1029
1271
|
const panel = this.#panel;
|
|
1030
1272
|
if (panel === null)
|
|
@@ -1148,6 +1390,7 @@ export class Editor {
|
|
|
1148
1390
|
return (this.#panel === null &&
|
|
1149
1391
|
!this.#menuOpen &&
|
|
1150
1392
|
!this.#atUp() && // KC3 §3: the @ picker owns the keys while up, exactly like the menu
|
|
1393
|
+
!this.#pickUp() && // TUI2-R2 ②: and so does the session picker — `?` is a query character there
|
|
1151
1394
|
this.#historyIdx === null &&
|
|
1152
1395
|
!this.#queuePopMode &&
|
|
1153
1396
|
!this.#pasting &&
|
|
@@ -1180,6 +1423,12 @@ export class Editor {
|
|
|
1180
1423
|
this.#onRender();
|
|
1181
1424
|
}
|
|
1182
1425
|
#submit() {
|
|
1426
|
+
// TUI2-R2 ②: the session picker takes Enter before anything else —
|
|
1427
|
+
// while it is up there is no turn to submit and no line to send.
|
|
1428
|
+
if (this.#pickUp()) {
|
|
1429
|
+
this.#pickAccept();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1183
1432
|
// KC3 §3: Enter ACCEPTS while the picker is up — the same rule the
|
|
1184
1433
|
// menu's A1 feel established (complete first, let the user read
|
|
1185
1434
|
// what they got, and let the NEXT Enter send it). An @ reference
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* editor, the diff renderer, and the palette.
|
|
7
7
|
*/
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
|
|
9
|
-
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
|
|
9
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
|
|
10
10
|
export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
11
11
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
12
12
|
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
@@ -14,6 +14,7 @@ export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResu
|
|
|
14
14
|
export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus, type StatusMeter } from "./status.js";
|
|
15
15
|
export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
|
|
16
16
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact } from "./strings.js";
|
|
17
|
-
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
17
|
+
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
18
|
+
export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, type SessionCardView, type SessionPickState, } from "./session-picker.js";
|
|
18
19
|
export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type AskStep, } from "./ask-panel.js";
|
|
19
|
-
export { KEY_BINDINGS, PANEL_KEYS_ROW, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
|
|
20
|
+
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,9 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
|
|
|
9
9
|
// W21 (the v8 approval round): the approval panel — the bounded block
|
|
10
10
|
// that replaces the running tool's live window while a human-chain
|
|
11
11
|
// approval is pending (the shape authority is the committed preview).
|
|
12
|
-
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
|
|
12
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
|
|
13
|
+
// TUI2-R2 ④: the pick payload — the panel slot's third occupant.
|
|
14
|
+
PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, } from "./approval-panel.js";
|
|
13
15
|
export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
|
|
14
16
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
15
17
|
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
@@ -26,7 +28,11 @@ export { contextRows, contextUnavailableRows } from "./context-ledger.js";
|
|
|
26
28
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView } from "./strings.js";
|
|
27
29
|
// KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
|
|
28
30
|
// deterministic rank, and the ONE cap the CLI's file source shares.
|
|
29
|
-
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun } from "./at-picker.js";
|
|
31
|
+
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun } from "./at-picker.js";
|
|
32
|
+
// TUI2-R2 ①–③: the session picker's pure half — the durability badge,
|
|
33
|
+
// the row (picked or printed), the band, and the filter. The CARDS are
|
|
34
|
+
// the cli's projection (session-cards.ts); this turns them into bytes.
|
|
35
|
+
export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, } from "./session-picker.js";
|
|
30
36
|
// KC3.5 (the ask round): the ask view — the panel machinery generalized.
|
|
31
37
|
// The cli composes the view and hands the answers to the tool; the keys,
|
|
32
38
|
// the rows and the walk are the terminal layer's.
|
|
@@ -35,4 +41,4 @@ export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, as
|
|
|
35
41
|
// honestly for a question nobody answered (the ① probe's surface).
|
|
36
42
|
// TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
|
|
37
43
|
// overlay and /help's keys row.
|
|
38
|
-
export { KEY_BINDINGS, PANEL_KEYS_ROW, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
|
|
44
|
+
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
|
|
3
|
+
* badge, the row, the band, and the filter.
|
|
4
|
+
*
|
|
5
|
+
* The badge is the round's whole argument. kiso's claim is that a
|
|
6
|
+
* session survives kill -9 and resumes from its durable prefix; until
|
|
7
|
+
* now that claim was a sentence in a README. A badge per row makes it a
|
|
8
|
+
* thing you can SEE before you pick: this one completed, this one was
|
|
9
|
+
* cut mid-run and will resume exactly, this one is holding a question
|
|
10
|
+
* for you.
|
|
11
|
+
*
|
|
12
|
+
* The vocabulary (the palette's functional set — no new colour):
|
|
13
|
+
*
|
|
14
|
+
* ✓ green the run's terminal event says completed
|
|
15
|
+
* ✗ red the terminal says anything else
|
|
16
|
+
* ▌ bold no terminal event — interrupted mid-run
|
|
17
|
+
* ? warn the uncertain ledger is not empty (overrides ▌)
|
|
18
|
+
* ◌ dim a permission request nobody has answered
|
|
19
|
+
*
|
|
20
|
+
* Purity, as everywhere in this package: the cards are DATA the CLI
|
|
21
|
+
* projects (session-cards.ts) and this module turns them into bytes. It
|
|
22
|
+
* never reads a session, never asks the runtime anything, and holds no
|
|
23
|
+
* state — which is what lets the picker band and the `kiso sessions`
|
|
24
|
+
* listing render from ONE definition instead of two that drift.
|
|
25
|
+
*/
|
|
26
|
+
/** The projected card — structurally what apps/cli/src/session-cards.ts
|
|
27
|
+
* produces. Declared here as the tui's INPUT contract (the package
|
|
28
|
+
* imports nothing from the runtime, by rule). */
|
|
29
|
+
export interface SessionCardView {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed";
|
|
32
|
+
readonly turns: number;
|
|
33
|
+
readonly updatedAt: number;
|
|
34
|
+
readonly uncertain: number;
|
|
35
|
+
readonly asks: number;
|
|
36
|
+
readonly outcome: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** The glyph per state — one cell each, so the badge column never
|
|
39
|
+
* shifts the id column (a column that moves per row reads as damage). */
|
|
40
|
+
export declare const BADGE_GLYPH: Readonly<Record<SessionCardView["badge"], string>>;
|
|
41
|
+
/** The badge, styled. The colour IS the meaning here (the mono
|
|
42
|
+
* discipline's three functional exceptions), so NO_COLOR degrades to
|
|
43
|
+
* the glyph alone — which is why the glyphs are distinct shapes and
|
|
44
|
+
* not three coloured dots. */
|
|
45
|
+
export declare function sessionBadge(badge: SessionCardView["badge"]): string;
|
|
46
|
+
/**
|
|
47
|
+
* What the row SAYS about the state. The interrupted note is the
|
|
48
|
+
* product's promise stated in the place the promise matters: the run
|
|
49
|
+
* continues from its durable prefix, so picking this row costs nothing
|
|
50
|
+
* that was already paid for.
|
|
51
|
+
*
|
|
52
|
+
* The ✗ note names the OUTCOME rather than flattening six endings into
|
|
53
|
+
* one word — "aborted" and "max turns" are different things to have
|
|
54
|
+
* happened, and a picker that calls both "failed" teaches the user
|
|
55
|
+
* nothing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sessionNote(card: SessionCardView): string;
|
|
58
|
+
/** The compact age — the picker's column, not the banner's sentence.
|
|
59
|
+
* `relativeTime` says "3d ago"; a column of ages does not need the
|
|
60
|
+
* word repeated on every row. */
|
|
61
|
+
export declare function sessionAge(updatedAt: number, now: number): string;
|
|
62
|
+
/** The id column's width — computed over EVERY card, never over the
|
|
63
|
+
* filtered subset, so the columns do not jump while the user types
|
|
64
|
+
* (the whole reason a filter-as-you-type picker is usable). */
|
|
65
|
+
export declare function idColumn(cards: readonly SessionCardView[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* The filter — the @ picker's muscle, aimed at the session id: a
|
|
68
|
+
* case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
|
|
69
|
+
* then by id length, then lexically. Identical determinism, identical
|
|
70
|
+
* feel; a row under the cursor never moves because two ids tied.
|
|
71
|
+
*
|
|
72
|
+
* An empty query matches everything and keeps the caller's order (the
|
|
73
|
+
* listing's newest-first), because "no query" is not a search — it is
|
|
74
|
+
* the list.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sessionFilter(cards: readonly SessionCardView[], query: string): SessionCardView[];
|
|
77
|
+
/**
|
|
78
|
+
* ONE picker row. The selection is a FULL-ROW reverse bar — the R1.5
|
|
79
|
+
* ⑧ ruling's shape, shared with the @ picker and the user chip: a
|
|
80
|
+
* two-cell marker in an eighty-column row is a selection you have to
|
|
81
|
+
* hunt for.
|
|
82
|
+
*
|
|
83
|
+
* The inner spans close with rvEnd inside the bar (never SGR 0, which
|
|
84
|
+
* would punch a hole in it) — the same composition atRow uses.
|
|
85
|
+
*/
|
|
86
|
+
export declare function sessionRow(card: SessionCardView, selected: boolean, W: number, now: number, idCol: number): string;
|
|
87
|
+
/** The counter row — the SELECTION's 1-based place in the whole
|
|
88
|
+
* filtered list, which the visible window cannot tell the user. */
|
|
89
|
+
export declare function sessionCounterRow(selected: number, total: number, W: number): string;
|
|
90
|
+
/** The picker's bound state — the editor owns it, the compositor reads
|
|
91
|
+
* it (the @ picker's contract, one surface over). */
|
|
92
|
+
export interface SessionPickState {
|
|
93
|
+
readonly cards: readonly SessionCardView[];
|
|
94
|
+
readonly matches: readonly SessionCardView[];
|
|
95
|
+
readonly selected: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
|
|
99
|
+
* itself or it reads as more scrollback), at most AT_VISIBLE windowed
|
|
100
|
+
* rows, then the counter. Returned as plain strings for the menu-rows
|
|
101
|
+
* channel, which already accounts them in chromeRows — the picker needs
|
|
102
|
+
* no geometry of its own, which is the entire reason it rides that
|
|
103
|
+
* channel.
|
|
104
|
+
*/
|
|
105
|
+
export declare function sessionPickerRows(state: SessionPickState, W: number, now: number): string[];
|
|
106
|
+
/** Slice ③ — the `kiso sessions` TTY row: the SAME projection, printed
|
|
107
|
+
* rather than picked. No selection bar (nothing is selected on a
|
|
108
|
+
* listing) and no leading indent: this row starts at column 1 like
|
|
109
|
+
* every other line a shell command prints. */
|
|
110
|
+
export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number): string;
|
|
111
|
+
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
112
|
+
* user can do next. */
|
|
113
|
+
export declare function sessionListFooter(count: number, W: number): string;
|