@vincemakes/kiso-tui 0.39.2 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/editor.js CHANGED
@@ -46,13 +46,20 @@ import { PickInput } from "./pick-input.js";
46
46
  // "you>" text is gone (the brick IS the prompt; the pipe path's readline
47
47
  // prompt keeps its own "you> " — v2a line mode, byte-for-byte).
48
48
  /**
49
- * TUI2-R3v2 ② — the mouse-mode bytes, stated once.
49
+ * TUI2-R3v2 ②, corrected by the owner's ruling of 2026-09-21 (finding
50
+ * DC-56) — the mouse-mode bytes, stated once.
50
51
  *
51
52
  * ?1000 is the button-event report and ?1006 is the SGR encoding that
52
53
  * makes it parseable past column 95 (the legacy X10 encoding packs the
53
- * coordinate into one byte and simply breaks on a wide terminal). Both
54
- * go on together and come off together; a terminal left with either one
55
- * set is a terminal that prints escape bytes at the shell prompt.
54
+ * coordinate into one byte and simply breaks on a wide terminal). kiso
55
+ * still RESETS them — on entry (a previous process may have died with
56
+ * reporting left on) and on exit — but it no longer ENABLES them, because
57
+ * a terminal that is reporting stops scrolling its own scrollback: a
58
+ * surface that takes the mouse takes the history with it. kiso's
59
+ * transcript lives in that scrollback by design, and the first version of
60
+ * this feature locked it exactly where a person most wants to read back —
61
+ * under a blocking question. The history wins: no surface claims the
62
+ * mouse, and every surface keeps its keys.
56
63
  */
57
64
  /**
58
65
  * R5 — the viewer's key table, as a pure function of the input chunk.
@@ -105,12 +112,11 @@ const ALT_WORD = new Map([
105
112
  ["\x7f", "killBack"],
106
113
  ["\x08", "killBack"],
107
114
  ]);
108
- export const MOUSE_ON = "\x1b[?1000h\x1b[?1006h";
109
115
  export const MOUSE_OFF = "\x1b[?1000l\x1b[?1006l";
110
116
  export const PROMPT = "▌ ";
111
117
  export const PROMPT_WIDTH = displayWidth(PROMPT);
112
118
  export const MENU_ITEMS = [
113
- { name: "/mode", desc: "switch the approval tier (manual/default/accept-edits/plan/bypass)" },
119
+ { name: "/mode", desc: "switch the approval tier (default/accept-edits/plan/dontAsk/bypass)" },
114
120
  { name: "/model", desc: "list model profiles; switch with /model <name|provider/model>" },
115
121
  { name: "/compact", desc: "summarize the older conversation to free context" },
116
122
  // the /resume+/clear mini-spec: the session-navigation pair
@@ -130,6 +136,10 @@ export const MENU_ITEMS = [
130
136
  { name: "/status", desc: "show session id, event count, and context estimate" },
131
137
  // TUI2-R1 (E): the rent-ledger attribution — where the context went
132
138
  { name: "/context", desc: "show where the context went — the last request's rent ledger" },
139
+ // 0.40.0: the person's door to the installed skills — `/<name>` works
140
+ // too when no command here has that name.
141
+ { name: "/skills", desc: "list the installed skills — /<name> [args] runs one" },
142
+ { name: "/skill", desc: "run a skill as your turn: /skill <name> [args]" },
133
143
  { name: "/help", desc: "print this list of commands" },
134
144
  ];
135
145
  /** KC1 §3 — the newline code point. Every source (paste, Ctrl+J, the
@@ -310,12 +320,16 @@ export class Editor {
310
320
  /** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
311
321
  * habitual trailing enter rather than submitting the restored draft. */
312
322
  #swallowEnter = false;
313
- /** TUI2-R3v2 ②: whether SGR 1006 reporting is currently enabled. */
314
- #mouseOn = false;
315
323
  /** TUI2-R3v2 ②: where the compositor put the panel's option rows this
316
324
  * frame (absolute 1-based screen rows). The editor owns no geometry —
317
325
  * it asks the surface that placed them. */
318
326
  #panelRows = null;
327
+ /** B (review of this round): the pick panel's window, published by the
328
+ * surface that DREW it — the frame's budget decides the size, so the
329
+ * editor PULLS it here instead of letting the input layer re-derive one
330
+ * (two derivations are two sizes, and on a short terminal the digits named
331
+ * rows nobody could see). */
332
+ #pickWin = null;
319
333
  #lineCb = null;
320
334
  #pendingLines = []; // submits before onLine is wired (startup) — never dropped
321
335
  #sigintCb = null;
@@ -341,6 +355,14 @@ export class Editor {
341
355
  * KEY, the CLI owns what it means. */
342
356
  #copyCbs = [];
343
357
  #onRender;
358
+ /** Item 6: true while a chunk of keys is being fed — the render hook
359
+ * is told a frame is key-originated, and the compositor paints it on
360
+ * the next tick instead of waiting out the trailing frame window. */
361
+ #feeding = false;
362
+ /** Item 6: set when a key in this chunk handed control to the run —
363
+ * a submitted line or a panel verdict. What follows is run state, not
364
+ * typing, so its frames keep the run's trailing window. */
365
+ #handedOff = false;
344
366
  /** TUI2-R1 (D): the keys sheet — a static one-screen overlay opened by
345
367
  * `?` on an empty composer and closed by the next key, whatever it
346
368
  * is. Deliberately a BOOLEAN and not a panel: the panel machinery
@@ -348,6 +370,9 @@ export class Editor {
348
370
  * buffer), and the sheet has no interaction to speak of. */
349
371
  #sheetOpen = false;
350
372
  #menuOpen = false; // v3 §04: the slash-command menu
373
+ /** 0.40.1: the menu's extra entries (the installed skills), read LIVE on
374
+ * every keystroke — a skill added by /reload appears without rebinding. */
375
+ #menuExtras = () => [];
351
376
  #menuSel = 0;
352
377
  // KC3 §3 — the @ file picker. THREE fields and no more: the armed
353
378
  // bit, the selection, and the per-open SNAPSHOT of the file list.
@@ -420,7 +445,7 @@ export class Editor {
420
445
  #closedResolve;
421
446
  closed;
422
447
  constructor(onRender) {
423
- this.#onRender = onRender;
448
+ this.#onRender = () => onRender(this.#feeding && !this.#handedOff);
424
449
  this.#onData = (raw) => this.feed(raw);
425
450
  this.closed = new Promise((resolve) => {
426
451
  this.#closedResolve = resolve;
@@ -510,7 +535,6 @@ export class Editor {
510
535
  // the human has no way to ask for it back.
511
536
  process.stdin.setRawMode(true);
512
537
  process.stdout.write(MOUSE_OFF);
513
- this.#mouseOn = false;
514
538
  process.stdout.write("\x1b[?2004h");
515
539
  process.stdin.on("data", this.#onData);
516
540
  }
@@ -794,11 +818,28 @@ export class Editor {
794
818
  const line = this.line();
795
819
  if (!line.startsWith("/"))
796
820
  return [];
797
- return MENU_ITEMS.filter((m) => m.name.startsWith(line));
821
+ // 0.40.1: the built-ins first, then the extras (skills) — and a
822
+ // built-in WINS a shared name, so a skill named like a command is
823
+ // never listed twice and never shadows it (the dispatcher's rule)
824
+ const builtins = new Set(MENU_ITEMS.map((m) => m.name));
825
+ const all = [...MENU_ITEMS, ...this.#menuExtras().filter((m) => !builtins.has(m.name))];
826
+ return all.filter((m) => m.name.startsWith(line));
827
+ }
828
+ /** 0.40.1 — bind the menu's extra entries (the CLI binds the installed
829
+ * skills). A function, read per keystroke, never a snapshot. */
830
+ bindMenuExtras(extras) {
831
+ this.#menuExtras = extras;
798
832
  }
799
833
  #refreshMenu() {
800
- if (this.#panelInput.up())
801
- return; // W21: the menu never opens while the panel owns the keys
834
+ if (this.#panelInput.up()) {
835
+ // W21: the menu never opens while the panel owns the keys — but
836
+ // the key that got here still changed the line, and insert and
837
+ // delete render only through this method (item 6: returning
838
+ // before the render left a panel's typed text to ride the next
839
+ // spinner tick, 0–150 ms late, or no tick at all).
840
+ this.#onRender();
841
+ return;
842
+ }
802
843
  const f = this.#menuFiltered();
803
844
  this.#menuOpen = f.length > 0;
804
845
  if (this.#menuSel >= f.length)
@@ -938,8 +979,8 @@ export class Editor {
938
979
  }
939
980
  // ── TUI2-R2 ② — the session picker ───────────────────────────────
940
981
  /** Open the picker on a bound card source (PickInput, S5). */
941
- beginPick(cards, onPick) {
942
- this.#pickInput.begin(cards, onPick);
982
+ beginPick(cards, onPick, here) {
983
+ this.#pickInput.begin(cards, onPick, here);
943
984
  }
944
985
  pickState() {
945
986
  return this.#pickInput.state();
@@ -957,7 +998,15 @@ export class Editor {
957
998
  * stashed and restored at close, the panel takes the keys and the
958
999
  * input row's lead, the composer's own bands close. */
959
1000
  beginPanel(view, onCommit, opts) {
960
- this.#panelInput.begin(view, onCommit, opts);
1001
+ const handOff = (v) => {
1002
+ // item 6: a verdict hands the frame to the run. The panel already
1003
+ // asked for a key frame on this key; the non-key render after the
1004
+ // verdict demotes it to the run's window.
1005
+ this.#handedOff = true;
1006
+ onCommit(v);
1007
+ this.#onRender();
1008
+ };
1009
+ this.#panelInput.begin(view, handOff, opts);
961
1010
  }
962
1011
  /** W21: cancel the panel — the SIGINT path's pair to beginPanel. */
963
1012
  cancelPanel() {
@@ -998,7 +1047,6 @@ export class Editor {
998
1047
  // chance this process gets, and emitting six harmless bytes twice
999
1048
  // is not a cost worth reasoning about.
1000
1049
  process.stdout.write(MOUSE_OFF);
1001
- this.#mouseOn = false;
1002
1050
  process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
1003
1051
  // REL-0161: the hardware cursor was hidden for the session's whole
1004
1052
  // life (the compositor's entry reset); this is the one place kiso
@@ -1010,30 +1058,20 @@ export class Editor {
1010
1058
  this.#closedResolve();
1011
1059
  }
1012
1060
  /**
1013
- * TUI2-R3v2 ② — mouse reporting follows the SELECTION SURFACES and
1014
- * nothing else.
1061
+ * TUI2-R3v2 ②, corrected by the owner's ruling of 2026-09-21 (finding
1062
+ * DC-56) — the bands still tell their host when a selection surface
1063
+ * opens or closes, and the host's answer is now "leave the terminal
1064
+ * alone".
1015
1065
  *
1016
- * While it is on, the terminal's own text selection changes behaviour
1017
- * (shift+drag still selects on every terminal that matters, but plain
1018
- * drag-to-copy does not), so leaving it on for the whole session would
1019
- * tax every copy-paste in the product to pay for a gesture that only
1020
- * means something while a list is up. It goes on when one opens and
1021
- * off when it closes — and both calls are idempotent, because the
1022
- * surfaces nest (a panel can open over a picker) and the bytes must
1023
- * not depend on the order they unwind in.
1066
+ * The bands' contract is unchanged — a band reports that it has taken
1067
+ * the composer — but the mouse is not a thing kiso hands out: reporting
1068
+ * on means the terminal stops scrolling its own scrollback, and every
1069
+ * band kiso has (the approval/ask/pick panel, the session picker, the @
1070
+ * picker) is a keyboard surface — digits, arrows, enter, esc, a filter.
1071
+ * The call sites stay so a future surface that DOES want the mouse has
1072
+ * one place to ask, and the ruling is written at that place.
1024
1073
  */
1025
- #setMouse(on) {
1026
- if (this.#mouseOn === on)
1027
- return;
1028
- this.#mouseOn = on;
1029
- if (this.#entered)
1030
- process.stdout.write(on ? MOUSE_ON : MOUSE_OFF);
1031
- }
1032
- /** The surfaces that own a selection — the approval/ask/pick panel, the
1033
- * session picker and the @ picker. Any one of them up = reporting on. */
1034
- #syncMouse() {
1035
- this.#setMouse(this.#panelInput.up() || this.#pickInput.up() || this.#atUp());
1036
- }
1074
+ #syncMouse() { }
1037
1075
  /** OR-11 (a) — the lead the composer's row is DRAWN with, which is not
1038
1076
  * always the brick. The CLI binds the compositor's input lead as `""`
1039
1077
  * (a prompt character is a third thing saying "input lives here", and
@@ -1074,6 +1112,21 @@ export class Editor {
1074
1112
  // ---- input ----
1075
1113
  /** Feed raw stdin bytes — the parser. Public for unit tests. */
1076
1114
  feed(raw) {
1115
+ const outer = this.#feeding;
1116
+ const outerHandedOff = this.#handedOff;
1117
+ this.#feeding = true;
1118
+ if (!outer)
1119
+ this.#handedOff = false;
1120
+ try {
1121
+ this.#feedKeys(raw);
1122
+ }
1123
+ finally {
1124
+ this.#feeding = outer;
1125
+ if (!outer)
1126
+ this.#handedOff = outerHandedOff;
1127
+ }
1128
+ }
1129
+ #feedKeys(raw) {
1077
1130
  const text = this.#pending + this.#decoder.decode(raw, { stream: true });
1078
1131
  this.#pending = "";
1079
1132
  // TUI2-R1 (D): the sheet is up — ANY key closes it, and the key
@@ -1512,6 +1565,10 @@ export class Editor {
1512
1565
  }
1513
1566
  i += 1;
1514
1567
  }
1568
+ else if (c === "\t" && this.#pickInput.toggleScope()) {
1569
+ // 0.40.0: the session picker's scope — this workspace ↔ all
1570
+ i += 1;
1571
+ }
1515
1572
  else if (c === "\t" && this.#atUp()) {
1516
1573
  // KC3 §3: Tab accepts the selected path — the token becomes
1517
1574
  // `@<path> `. Never the file's content.
@@ -1692,6 +1749,13 @@ export class Editor {
1692
1749
  * and context-menu everywhere else and would mean "approve" here.
1693
1750
  * The stakes are a side effect the human did not ask for, and an
1694
1751
  * ambiguous mouse event is not consent.
1752
+ *
1753
+ * 2026-09-21 (finding DC-56): kiso no longer ENABLES reporting, so on a
1754
+ * terminal this path is reached only when something else left the mouse
1755
+ * on — and then a click still answers the panel. The wheel stays
1756
+ * dropped either way (a scroll past a panel is not a choice), which is
1757
+ * the other half of why the settled rule is "no surface takes the
1758
+ * mouse": a terminal that is reporting cannot scroll its own history.
1695
1759
  */
1696
1760
  #mouseEvent(params, press) {
1697
1761
  if (!press)
@@ -1712,6 +1776,11 @@ export class Editor {
1712
1776
  bindPanelRows(fn) {
1713
1777
  this.#panelRows = fn;
1714
1778
  }
1779
+ /** B: the pick window the last frame drew — the Dock's value, not the
1780
+ * editor's guess. Same pull-on-demand shape as `bindPanelRows`. */
1781
+ bindPickWindow(fn) {
1782
+ this.#pickWin = fn;
1783
+ }
1715
1784
  /** TMUX-F1 ②: `run` identical arrow sequences arrived in ONE read. Every
1716
1785
  * surface but one gets every press — a wheel over the transcript viewer,
1717
1786
  * a panel, a multi-row draft scrolls, as a wheel should. The HISTORY
@@ -1941,6 +2010,7 @@ export class Editor {
1941
2010
  reflow: () => this.#reflow(),
1942
2011
  render: () => this.#onRender(),
1943
2012
  syncMouse: () => this.#syncMouse(),
2013
+ pickWindow: () => this.#pickWin?.() ?? null,
1944
2014
  closeBands: () => {
1945
2015
  this.#menuOpen = false;
1946
2016
  this.#menuSel = 0;
@@ -2436,6 +2506,7 @@ export class Editor {
2436
2506
  // recalls a readable line that still expands when it is sent
2437
2507
  // again (the map outlives the buffer, by design).
2438
2508
  const sent = this.#expandPastes(line);
2509
+ this.#handedOff = true;
2439
2510
  const cb = this.#questionCb;
2440
2511
  this.#questionCb = null;
2441
2512
  if (cb !== null) {
@@ -2449,7 +2520,7 @@ export class Editor {
2449
2520
  }
2450
2521
  if (cb === null && line !== "")
2451
2522
  this.#remember(line);
2452
- this.#onRender();
2523
+ this.#onRender(); // item 6: #handedOff is set, so this render is non-key — the submit's frame keeps the run's window
2453
2524
  }
2454
2525
  /** A2: step the history browse; a delta past the newest exits back to
2455
2526
  * the pre-browse input. */
package/dist/index.d.ts CHANGED
@@ -12,12 +12,12 @@ export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component,
12
12
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
13
13
  export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, 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 "./lines.js";
14
14
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
15
- export { STATUS_GLYPHS, cacheHitPct, decodeRate, idleStatus, runningStatus, type StatusMeter } from "./status.js";
15
+ export { STATUS_GLYPHS, cacheHitPct, compactingStatus, composeRow, decodeRate, idleStatus, retrySegment, runningStatus, type CompactingProgress, type RetryOnRow, type RowSegment, type StatusMeter } from "./status.js";
16
16
  export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
17
17
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, verifyOfferView, type TrustArtifact } from "./strings.js";
18
18
  export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
19
- export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, type SessionCardView, type SessionPickState, } from "./session-picker.js";
19
+ export { idColumn, sessionAge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows, sessionRow, scopeSessions, scopeTitle, type PickScopeState, type SessionCardView, type SessionPickState, } from "./session-picker.js";
20
20
  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";
21
21
  export { resolveGround, type Ground } from "@vincemakes/kiso-tui-cells";
22
22
  export { settledLabel } from "@vincemakes/kiso-tui-cells";
23
- export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
23
+ export { KEY_BINDINGS, PANEL_KEYS_ROW, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTermi
19
19
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
20
20
  // KC2 §5: the status rows' formatters — the CLI keeps the state and the
21
21
  // repaint, the terminal layer owns what the row says.
22
- export { STATUS_GLYPHS, cacheHitPct, decodeRate, idleStatus, runningStatus } from "./status.js";
22
+ export { STATUS_GLYPHS, cacheHitPct, compactingStatus, composeRow, decodeRate, idleStatus, retrySegment, runningStatus } from "./status.js";
23
23
  // TUI2-R1 (E): /context's attribution rows — a pure function of the
24
24
  // counts the trace sidecar already records (the CLI reads, this renders).
25
25
  export { contextRows, contextUnavailableRows } from "./context-ledger.js";
@@ -33,7 +33,10 @@ export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow,
33
33
  // TUI2-R2 ①–③: the session picker's pure half — the durability badge,
34
34
  // the row (picked or printed), the band, and the filter. The CARDS are
35
35
  // the cli's projection (session-cards.ts); this turns them into bytes.
36
- export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, } from "./session-picker.js";
36
+ export { idColumn, sessionAge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows, sessionRow,
37
+ // 0.40.0: the workspace scope — pure, so `kiso sessions` and the picker
38
+ // scope by one rule
39
+ scopeSessions, scopeTitle, } from "./session-picker.js";
37
40
  // KC3.5 (the ask round): the ask view — the panel machinery generalized.
38
41
  // The cli composes the view and hands the answers to the tool; the keys,
39
42
  // the rows and the walk are the terminal layer's.
@@ -46,4 +49,4 @@ export { resolveGround } from "@vincemakes/kiso-tui-cells";
46
49
  // one duration form: the CLI's own surfaces label a settled duration the
47
50
  // way a settled card does, rather than writing a second one.
48
51
  export { settledLabel } from "@vincemakes/kiso-tui-cells";
49
- export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView } from "./strings.js";
52
+ export { KEY_BINDINGS, PANEL_KEYS_ROW, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView } from "./strings.js";
package/dist/lines.d.ts CHANGED
@@ -169,6 +169,11 @@ export interface RecapStats {
169
169
  * passed only when above the noise floor — the re-sent-uncached
170
170
  * prefix. Absent → the recap bytes stay the historical form. */
171
171
  readonly missed?: number;
172
+ /** 0.40.0 (the owner): the turn's first request came this many minutes
173
+ * after the session's last bill, and the prompt cache had gone cold —
174
+ * the recap says so instead of a bare `fresh 730k · cache 0%`, which
175
+ * is true and reads like a fault. Absent → the historical form. */
176
+ readonly coldAfterMinutes?: number;
172
177
  readonly ctxLeftPct: number | null;
173
178
  /** R3g — the terminal's width. The recap is the ONE row on the screen
174
179
  * that was never measured: it is written raw, so a line longer than
package/dist/lines.js CHANGED
@@ -96,8 +96,8 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
96
96
  case "compacted":
97
97
  return { text: `${p.dim} [compacted ${ev.cleared.length} results]${p.reset}\n`, newline: true, prompt: false };
98
98
  case "summarized":
99
- // ADR-0044: the /compact event is OFF-LOOP — it never appears in
100
- // a run stream; rendered for the switch's completeness only.
99
+ // ADR-0044: /compact's event is off-loop; since ADR-0055 Amendment 1
100
+ // the in-run tiers also append one mid-run, and this is its line.
101
101
  return { text: `${p.dim} [summarized up to seq ${ev.coversToSeq}]${p.reset}\n`, newline: true, prompt: false };
102
102
  case "uncertain_pending":
103
103
  return {
@@ -211,7 +211,16 @@ export function renderRecap(s) {
211
211
  ? [`${s.tools} tool${s.tools === 1 ? "" : "s"}${edits > 0 ? ` (${edits} edit${edits === 1 ? "" : "s"})` : ""}`]
212
212
  : [];
213
213
  const parts = [`took ${elapsedLabel(s.seconds)}`, ...work];
214
- if (s.usage.known) {
214
+ // The cold turn: the gap and the re-read are the FACTS and come first,
215
+ // so a cut from the end never reaches them; "cache cold after" is the
216
+ // label, and it is the first thing shortened when the row is narrow.
217
+ const cold = s.coldAfterMinutes !== undefined && s.usage.known && s.usage.in !== null;
218
+ if (cold) {
219
+ parts.push(`cache cold after ${s.coldAfterMinutes} min`, `re-read ${kUnit(s.missed ?? s.usage.in ?? 0)}`);
220
+ if (s.usage.out !== null)
221
+ parts.push(`out ${kUnit(s.usage.out)}`);
222
+ }
223
+ else if (s.usage.known) {
215
224
  const seg = `${s.usage.in !== null ? `fresh ${kUnit(s.usage.in)}` : ""}${s.usage.in !== null && s.usage.out !== null ? " " : ""}${s.usage.out !== null ? `out ${kUnit(s.usage.out)}` : ""}`;
216
225
  if (seg !== "")
217
226
  parts.push(seg);
@@ -230,7 +239,13 @@ export function renderRecap(s) {
230
239
  // R3g: ONE physical row, at any width — the same rule every other row
231
240
  // in the product obeys. The cut is the honest "…": the recap said
232
241
  // more than fits, and says so.
233
- const line = parts.join(" · ");
242
+ let line = parts.join(" · ");
243
+ // A cold turn that does not fit sheds its LABEL first, then the cut
244
+ // below takes the tail — the gap and the re-read stand in front of it.
245
+ if (cold && s.width !== undefined && s.width >= 20 && visibleWidth(`✦ ${line}`) > s.width) {
246
+ parts[parts.indexOf(`cache cold after ${s.coldAfterMinutes} min`)] = `cold ${s.coldAfterMinutes} min`;
247
+ line = parts.join(" · ");
248
+ }
234
249
  // R3g: the floor is the renderer's own guard against a caller that
235
250
  // hands it a degenerate width (a PTY with no winsize reports 0). A
236
251
  // recap cut to one character is worse than one that wraps.
@@ -32,6 +32,14 @@ export interface BandHost {
32
32
  line(): string;
33
33
  /** The buffer with its paste capsules expanded — the text that would leave the editor. */
34
34
  expandPastes(line: string): string;
35
+ /** The pick panel's window as the RENDERER drew it (review of this round):
36
+ * the size depends on the frame's budget, so the digit keys must be handed
37
+ * the value instead of deriving it again from PICK_MAX. Absent → the input
38
+ * layer falls back to its own derivation. */
39
+ pickWindow?(): {
40
+ first: number;
41
+ size: number;
42
+ } | null;
35
43
  /** Empty the buffer: chars, cursor and the ↑↓ goal column. */
36
44
  clear(): void;
37
45
  /** Type one code point at the cursor. */
@@ -16,7 +16,7 @@
16
16
  * and the mouse hit. The bodies are the same; only the buffer access
17
17
  * goes through the host.
18
18
  */
19
- import { PICK_MAX, panelOptions, saferDegradedNote, startLevel, stepLevel, } from "./approval-panel.js";
19
+ import { PICK_MAX, panelOptions, pickWindow, saferDegradedNote, startLevel, stepLevel, } from "./approval-panel.js";
20
20
  import { askCommitCustom, askKey, askOnCustomRow, askStart } from "./ask-panel.js";
21
21
  export class PanelInput {
22
22
  host;
@@ -206,7 +206,11 @@ export class PanelInput {
206
206
  if (panel === null)
207
207
  return false;
208
208
  if (panel.pick !== null && panel.pick.phase === "options") {
209
- const n = Math.min(panel.view.pick.options.length, PICK_MAX);
209
+ // DC-58: EVERY option is reachable by ↑↓ — the window follows the
210
+ // cursor (`pickWindow`) and PICK_MAX is only how many rows one
211
+ // screen holds. This bound used to be `min(count, PICK_MAX)`, which
212
+ // is what left a 60-profile config with nine reachable rows.
213
+ const n = panel.view.pick.options.length;
210
214
  const cur = panel.pick.cursor;
211
215
  const next = dir === "up" ? Math.max(0, cur - 1) : Math.min(Math.max(0, n - 1), cur + 1);
212
216
  // OR-7: the second axis belongs to the highlighted option, so it
@@ -448,9 +452,18 @@ export class PanelInput {
448
452
  const panel = this.#panel;
449
453
  if (panel === null || panel.pick === null)
450
454
  return;
451
- if (index < 0 || index >= Math.min(panel.view.pick.options.length, PICK_MAX))
455
+ const count = panel.view.pick.options.length;
456
+ // DC-58: a digit names the row ON SCREEN. The window's origin is what
457
+ // turns a visible position into an option index — the same
458
+ // `pickWindow` the renderer drew the rows with, so what the row says
459
+ // and what the key does cannot drift apart.
460
+ const { first, size } = this.host.pickWindow?.() ?? pickWindow(panel.pick.cursor, count, PICK_MAX);
461
+ if (index < 0 || index >= size)
452
462
  return;
453
- panel.pick = { cursor: index, phase: "options", level: startLevel(panel.view.pick.options[index]) };
463
+ const target = first + index;
464
+ if (target >= count)
465
+ return;
466
+ panel.pick = { cursor: target, phase: "options", level: startLevel(panel.view.pick.options[target]) };
454
467
  this.host.render();
455
468
  }
456
469
  #pickPanelEnter() {
@@ -9,13 +9,17 @@ export declare class PickInput {
9
9
  * (the buffer becomes the filter query) and `onPick` receives the
10
10
  * chosen id — or null when the human leaves without picking, which
11
11
  * is a first-class outcome and not an error. */
12
- begin(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void): void;
12
+ begin(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
13
13
  /** The picker's state, derived: the full card list (the id column
14
14
  * measures over ALL of them, so the columns never jump), the
15
15
  * filtered matches, and the selection CLAMPED at read time — the
16
16
  * same correction discipline the @ picker uses, for the same
17
17
  * reason: narrowing can only ever shrink the list. */
18
18
  state(): SessionPickState | null;
19
+ /** 0.40.0 — tab flips CURRENT ↔ ALL. The filter owns every printable
20
+ * key (the buffer IS the query), so the toggle cannot be one. True when
21
+ * the picker owned the key. */
22
+ toggleScope(): boolean;
19
23
  /** The band's height estimate: the header + the windowed rows (or
20
24
  * the one "no match" row) + the counter. */
21
25
  rows(): number;
@@ -18,12 +18,16 @@
18
18
  * through the host.
19
19
  */
20
20
  import { AT_VISIBLE } from "./at-picker.js";
21
- import { sessionFilter } from "./session-picker.js";
21
+ import { scopeSessions, sessionFilter } from "./session-picker.js";
22
22
  export class PickInput {
23
23
  host;
24
24
  #cards = null;
25
25
  #commit = null;
26
26
  #sel = 0;
27
+ /** 0.40.0: the running workspace (undefined = an unscoped picker), and
28
+ * whether the person flipped to ALL. */
29
+ #here = undefined;
30
+ #all = false;
27
31
  constructor(host) {
28
32
  this.host = host;
29
33
  }
@@ -34,10 +38,12 @@ export class PickInput {
34
38
  * (the buffer becomes the filter query) and `onPick` receives the
35
39
  * chosen id — or null when the human leaves without picking, which
36
40
  * is a first-class outcome and not an error. */
37
- begin(cards, onPick) {
41
+ begin(cards, onPick, here) {
38
42
  this.#cards = cards;
39
43
  this.#commit = onPick;
40
44
  this.#sel = 0;
45
+ this.#here = here;
46
+ this.#all = false;
41
47
  this.host.syncMouse();
42
48
  this.host.clear();
43
49
  this.host.reflow();
@@ -51,15 +57,35 @@ export class PickInput {
51
57
  state() {
52
58
  if (this.#cards === null)
53
59
  return null;
54
- const cards = this.#cards();
60
+ // 0.40.0: the filter runs INSIDE the scope — typing narrows what the
61
+ // title says is showing, never reaches past it
62
+ const scoped = this.#here === undefined ? null : scopeSessions(this.#cards(), this.#here, this.#all);
63
+ const cards = scoped === null ? this.#cards() : scoped.cards;
55
64
  const matches = sessionFilter(cards, this.host.line());
56
- return { cards, matches, selected: Math.max(0, Math.min(this.#sel, matches.length - 1)) };
65
+ return { cards, matches, selected: Math.max(0, Math.min(this.#sel, matches.length - 1)), scope: scoped?.scope ?? null };
66
+ }
67
+ /** 0.40.0 — tab flips CURRENT ↔ ALL. The filter owns every printable
68
+ * key (the buffer IS the query), so the toggle cannot be one. True when
69
+ * the picker owned the key. */
70
+ toggleScope() {
71
+ const view = this.state();
72
+ if (view === null || view.scope === null || view.scope === undefined)
73
+ return false;
74
+ this.#all = !this.#all;
75
+ this.#sel = 0;
76
+ this.host.reflow();
77
+ this.host.render();
78
+ return true;
57
79
  }
58
80
  /** The band's height estimate: the header + the windowed rows (or
59
81
  * the one "no match" row) + the counter. */
60
82
  rows() {
61
83
  const view = this.state();
62
- return view === null ? 0 : Math.min(Math.max(view.matches.length, 1), AT_VISIBLE) + 2;
84
+ if (view === null)
85
+ return 0;
86
+ // 0.40.1: + the unknown-workspace row, when CURRENT shows it
87
+ const unknownRow = view.scope != null && !view.scope.all && view.scope.unknown > 0 ? 1 : 0;
88
+ return Math.min(Math.max(view.matches.length, 1), AT_VISIBLE) + 2 + unknownRow;
63
89
  }
64
90
  /** ↑↓: the selection walks the matches and stops at both ends. True
65
91
  * when the picker owned the key (the caller renders). */
@@ -78,6 +104,8 @@ export class PickInput {
78
104
  this.#cards = null;
79
105
  this.#commit = null;
80
106
  this.#sel = 0;
107
+ this.#here = undefined;
108
+ this.#all = false;
81
109
  this.host.syncMouse();
82
110
  this.host.clear();
83
111
  this.host.reflow();