@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/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 { view: panel.view, phase: panel.phase, sel: panel.sel, ...(panel.ask === null ? {} : { ask: panel.ask }) };
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
- if (c === "1" || c === "y" || c === "Y") {
606
- if (panel.phase === "options")
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
- i += 1;
609
- continue;
610
- }
611
- if (c === "2" && panel.phase === "options" && panel.view.flavor === "approval") {
612
- this.#panelRule();
613
- i += 1;
614
- continue;
615
- }
616
- if (c === "3" || c === "n" || c === "N") {
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
- i += 1;
620
- continue;
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.ask !== null && this.#panel.ask.phase === "options")
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, } from "./approval-panel.js";
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;