@vincemakes/kiso-tui 0.7.0 → 0.9.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.d.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { charWidth, displayWidth, widthOf } from "./width.js";
25
25
  export { charWidth, displayWidth, widthOf };
26
- import { type PanelState, type PanelVerdict, type PanelView } from "./approval-panel.js";
26
+ import type { PanelState, PanelVerdict, PanelView } from "./approval-panel.js";
27
27
  import { type AtItem, type AtMatch } from "./at-picker.js";
28
28
  export declare const PROMPT = "\u258C ";
29
29
  export declare const PROMPT_WIDTH: number;
@@ -60,6 +60,9 @@ export declare class Editor {
60
60
  bindQueue(state: () => readonly string[], pop: () => string | null): void;
61
61
  /** The whole buffer as text (the CLI's line()/clearLine()). */
62
62
  line(): string;
63
+ /** TUI2-R1 (D): whether the keys sheet is up — the compositor's slot
64
+ * read (bound like the menu and the picker). */
65
+ sheetOpen(): boolean;
63
66
  clearLine(): void;
64
67
  /** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
65
68
  * their legacy meaning (the CURSOR LINE's visible slice and the
package/dist/editor.js CHANGED
@@ -26,7 +26,9 @@ 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 { panelLead } from "./approval-panel.js";
29
+ // KC3.5: the panel-slot dispatchers — the ask branch folded into the
30
+ // W21 lead/rows, so this file keeps ONE panel and one key owner.
31
+ import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
30
32
  import { AT_VISIBLE, atFilter } from "./at-picker.js";
31
33
  // TUI v4 #16d: the input row is the blue brick + the edit area — the
32
34
  // "you>" text is gone (the brick IS the prompt; the pipe path's readline
@@ -40,6 +42,8 @@ export const MENU_ITEMS = [
40
42
  { name: "/think", desc: "show the last full thinking block" },
41
43
  { name: "/last", desc: "show the most recent tool call's input and output" },
42
44
  { name: "/status", desc: "show session id, event count, and context estimate" },
45
+ // TUI2-R1 (E): the rent-ledger attribution — where the context went
46
+ { name: "/context", desc: "show where the context went — the last request's rent ledger" },
43
47
  { name: "/help", desc: "print this list of commands" },
44
48
  ];
45
49
  /** KC1 §3 — the newline code point. Every source (paste, Ctrl+J, the
@@ -107,6 +111,12 @@ export class Editor {
107
111
  // coexist; the editor never interprets the key itself.
108
112
  #expandCbs = [];
109
113
  #onRender;
114
+ /** TUI2-R1 (D): the keys sheet — a static one-screen overlay opened by
115
+ * `?` on an empty composer and closed by the next key, whatever it
116
+ * is. Deliberately a BOOLEAN and not a panel: the panel machinery
117
+ * exists for interactions (a lead, a status, a reducer, a stashed
118
+ * buffer), and the sheet has no interaction to speak of. */
119
+ #sheetOpen = false;
110
120
  #menuOpen = false; // v3 §04: the slash-command menu
111
121
  #menuSel = 0;
112
122
  // KC3 §3 — the @ file picker. THREE fields and no more: the armed
@@ -192,6 +202,11 @@ export class Editor {
192
202
  line() {
193
203
  return String.fromCodePoint(...this.#chars);
194
204
  }
205
+ /** TUI2-R1 (D): whether the keys sheet is up — the compositor's slot
206
+ * read (bound like the menu and the picker). */
207
+ sheetOpen() {
208
+ return this.#sheetOpen;
209
+ }
195
210
  clearLine() {
196
211
  this.#chars = [];
197
212
  this.#cursor = 0;
@@ -439,6 +454,7 @@ export class Editor {
439
454
  view,
440
455
  phase: "options",
441
456
  sel: 0,
457
+ ask: view.ask === undefined ? null : askStart(view.ask),
442
458
  amend: "yes",
443
459
  onCommit,
444
460
  stash: { chars: this.#chars, cursor: this.#cursor, scroll: this.#scroll },
@@ -463,7 +479,7 @@ export class Editor {
463
479
  const panel = this.#panel;
464
480
  if (panel === null)
465
481
  return null;
466
- return { view: panel.view, phase: panel.phase, sel: panel.sel };
482
+ return { view: panel.view, phase: panel.phase, sel: panel.sel, ...(panel.ask === null ? {} : { ask: panel.ask }) };
467
483
  }
468
484
  enter() {
469
485
  if (this.#entered)
@@ -493,7 +509,7 @@ export class Editor {
493
509
  // W21: the panel's lead owns the row while up (the brick returns
494
510
  // when the panel closes).
495
511
  const panel = this.#panel;
496
- const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.sel) : `${p.bold}${PROMPT}${p.reset}`;
512
+ const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.sel, panel.ask ?? undefined) : `${p.bold}${PROMPT}${p.reset}`;
497
513
  // W23: the ONE width authority — leadWidth(lead), the ANSI-stripped
498
514
  // visible width (the styled panel lead / the styled brick measure
499
515
  // the same as their plain text — a lead can never measure
@@ -507,6 +523,17 @@ export class Editor {
507
523
  feed(raw) {
508
524
  const text = this.#pending + this.#decoder.decode(raw, { stream: true });
509
525
  this.#pending = "";
526
+ // TUI2-R1 (D): the sheet is up — ANY key closes it, and the key
527
+ // that closed it is CONSUMED. The whole chunk goes, deliberately:
528
+ // an arrow key is three bytes, and closing on the first while
529
+ // letting `[A` fall through as literal text would be a sheet that
530
+ // types into your composer on the way out. A dismissal costs one
531
+ // keystroke; that is the entire contract.
532
+ if (this.#sheetOpen) {
533
+ this.#sheetOpen = false;
534
+ this.#onRender();
535
+ return;
536
+ }
510
537
  let i = 0;
511
538
  while (i < text.length) {
512
539
  const c = text[i];
@@ -520,6 +547,45 @@ export class Editor {
520
547
  // text); ctrl-c still rides the SIGINT handler (which
521
548
  // cancels the panel).
522
549
  const panel = this.#panel;
550
+ // KC3.5: an ASK panel routes its own keys — the digits pick
551
+ // (single-select advances, multi toggles), space toggles at
552
+ // the cursor, `t` opens the type-your-own line (the
553
+ // rule-input phase's shape: the buffer is the editor's, so
554
+ // only esc and enter are intercepted while typing), esc
555
+ // declines the whole call. Everything else falls through to
556
+ // the ordinary editing chain below.
557
+ if (panel.ask !== null) {
558
+ const typing = panel.ask.phase === "custom";
559
+ if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
560
+ this.#askStep("esc");
561
+ i += 1;
562
+ continue;
563
+ }
564
+ if (c === "\x0d" || c === "\x0a") {
565
+ this.#askStep(typing ? "commit" : "enter");
566
+ i += 1;
567
+ continue;
568
+ }
569
+ if (!typing && (c === " " || (c !== undefined && c >= "1" && c <= "4") || c === "t" || c === "T")) {
570
+ this.#askStep(c === " " ? "space" : c === "T" ? "t" : c);
571
+ i += 1;
572
+ continue;
573
+ }
574
+ // an ask at rest swallows stray PRINTABLE keys — the panel
575
+ // owns them, and a typed "/" or "@" must not arm the menu
576
+ // or the picker underneath. Two things are never
577
+ // swallowed: the CSI/SS3 introducer, because ←/↑/↓ are
578
+ // the ask's own keys and the parser below routes them
579
+ // (the T-Q1 red), and the CONTROL characters, because
580
+ // ctrl-c must still reach the SIGINT handler that
581
+ // cancels the panel — W21's own rule, and what the T-Q6
582
+ // race red caught: an abort with the panel up did
583
+ // nothing at all.
584
+ if (!typing && c !== undefined && c >= " " && c !== "\x7f") {
585
+ i += 1;
586
+ continue;
587
+ }
588
+ }
523
589
  if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
524
590
  this.#panelEsc();
525
591
  i += 1;
@@ -710,6 +776,17 @@ export class Editor {
710
776
  cb();
711
777
  i += 1;
712
778
  }
779
+ else if (c === "?" && this.#composerIdle() && this.#chars.length === 0) {
780
+ // TUI2-R1 (D): `?` opens the keys sheet — but ONLY on an
781
+ // empty composer with nobody else holding the keys. Mid-text
782
+ // it is the question mark a human is typing, and #composerIdle
783
+ // already encodes "no panel, no menu, no picker, no browse".
784
+ // The precedence can only ever ADD: every state that used to
785
+ // insert a `?` still inserts one.
786
+ this.#sheetOpen = true;
787
+ this.#onRender();
788
+ i += 1;
789
+ }
713
790
  else if (c !== undefined && c < " ") {
714
791
  i += 1; // other control — ignored
715
792
  }
@@ -760,7 +837,10 @@ export class Editor {
760
837
  // panel owns the keys while up (↑↓ do nothing — the panel has no
761
838
  // ↑↓ role).
762
839
  if (this.#panel !== null) {
763
- /* the panel owns the keys */
840
+ // W21: the panel owns the keys. KC3.5: an ask uses ↑↓ for
841
+ // the option cursor (the approval panel still has no ↑↓ role).
842
+ if (this.#panel.ask !== null && this.#panel.ask.phase === "options")
843
+ this.#askStep(final === "A" ? "up" : "down");
764
844
  }
765
845
  else if (this.#menuOpen) {
766
846
  if (final === "A")
@@ -798,7 +878,12 @@ export class Editor {
798
878
  this.#onRender();
799
879
  }
800
880
  else if (final === "D") {
801
- this.#move(-1);
881
+ // KC3.5: ← walks the ask BACK a question (the ‹ n/m › walk); at
882
+ // question one it stays put — esc is the decline, never ←.
883
+ if (this.#panel?.ask != null && this.#panel.ask.phase === "options")
884
+ this.#askStep("left");
885
+ else
886
+ this.#move(-1);
802
887
  }
803
888
  else if (final === "C") {
804
889
  this.#move(1);
@@ -913,6 +998,33 @@ export class Editor {
913
998
  else if (panel.sel === 3)
914
999
  this.#panelClose({ action: "deny", reason: "" });
915
1000
  }
1001
+ /**
1002
+ * KC3.5 — one ask key: the pure reducer decides, this method applies.
1003
+ * The buffer is cleared on every phase change so the type-your-own
1004
+ * line starts empty and its text never leaks back into the options
1005
+ * (the rule-input phase's own discipline). A step that produced a
1006
+ * RESULT closes the panel with it — the stash/restore is the W21
1007
+ * path, identical for an answer and for a decline.
1008
+ */
1009
+ #askStep(key) {
1010
+ const panel = this.#panel;
1011
+ if (panel === null || panel.ask === null)
1012
+ return;
1013
+ const spec = panel.view.ask;
1014
+ const before = panel.ask.phase;
1015
+ const step = key === "commit" ? askCommitCustom(spec, panel.ask, this.line()) : askKey(spec, panel.ask, key);
1016
+ panel.ask = step.state;
1017
+ if (step.state.phase !== before) {
1018
+ this.#chars = [];
1019
+ this.#cursor = 0;
1020
+ this.#scroll = 0;
1021
+ }
1022
+ if (step.result !== undefined) {
1023
+ this.#panelClose({ action: "answers", result: step.result });
1024
+ return;
1025
+ }
1026
+ this.#onRender();
1027
+ }
916
1028
  #panelClose(verdict) {
917
1029
  const panel = this.#panel;
918
1030
  if (panel === null)
@@ -1162,7 +1274,7 @@ export class Editor {
1162
1274
  // W23: the ONE width authority — leadWidth(lead) — the cap follows
1163
1275
  // the lead the editor itself renders (the panel lead when the panel
1164
1276
  // owns the keys, the brick otherwise): maxW = W − walls − lead.
1165
- const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.sel) : PROMPT;
1277
+ const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.sel, this.#panel.ask ?? undefined) : PROMPT;
1166
1278
  const leadW = leadWidth(lead);
1167
1279
  const maxW = Math.max(1, W - leadW - 4); // W6: the box's walls (2+2) — the visible line fits the box's inner width; the "…" rides inside
1168
1280
  // KC1: the scroll is the CURSOR LINE's own offset — a single-line
package/dist/index.d.ts CHANGED
@@ -7,10 +7,13 @@
7
7
  */
8
8
  export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
9
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";
10
- export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
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";
13
13
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
14
- export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
14
+ export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus, type StatusMeter } from "./status.js";
15
+ export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
15
16
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact } from "./strings.js";
16
17
  export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
18
+ 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";
package/dist/index.js CHANGED
@@ -10,13 +10,16 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
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
12
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, } from "./approval-panel.js";
13
- export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
13
+ export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
14
14
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
15
15
  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";
16
16
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
17
17
  // KC2 §5: the status rows' formatters — the CLI keeps the state and the
18
18
  // repaint, the terminal layer owns what the row says.
19
- export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
19
+ export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus } from "./status.js";
20
+ // TUI2-R1 (E): /context's attribution rows — a pure function of the
21
+ // counts the trace sidecar already records (the CLI reads, this renders).
22
+ export { contextRows, contextUnavailableRows } from "./context-ledger.js";
20
23
  // KC3 §1 (the extraction): the human-facing strings — the prompt, the
21
24
  // project-trust listing/view/note, the uncertain execution's view. The
22
25
  // FLOW (who is asked, what a verdict means) stays in the cli.
@@ -24,3 +27,12 @@ export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrusted
24
27
  // KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
25
28
  // deterministic rank, and the ONE cap the CLI's file source shares.
26
29
  export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun } from "./at-picker.js";
30
+ // KC3.5 (the ask round): the ask view — the panel machinery generalized.
31
+ // The cli composes the view and hands the answers to the tool; the keys,
32
+ // the rows and the walk are the terminal layer's.
33
+ export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, } from "./ask-panel.js";
34
+ // KC3.5 §4: the interrupted-ask copy — the SAME uncertainty gate, said
35
+ // honestly for a question nobody answered (the ① probe's surface).
36
+ // TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
37
+ // overlay and /help's keys row.
38
+ export { KEY_BINDINGS, PANEL_KEYS_ROW, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
package/dist/status.d.ts CHANGED
@@ -33,6 +33,36 @@ export declare const STATUS_GLYPHS: readonly ["▖", "▘", "▝", "▗"];
33
33
  * because it is on screen exactly when the gesture is useful.
34
34
  */
35
35
  export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number): string;
36
+ /**
37
+ * TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
38
+ * to what it has left.
39
+ *
40
+ * Both fields are optional and both are omitted when unknown, because
41
+ * the row's job is to be true rather than complete:
42
+ *
43
+ * - `cacheHitPct` is cacheRead / (fresh + cacheRead) — the E2
44
+ * denominator (the pinned sentence: it cannot exceed 100%). A
45
+ * session with no usage yet has no cache hit rate, and an
46
+ * unmeasured cache is NOT a 0% cache, so it renders nothing.
47
+ * - `costUsd` is the CANONICAL cost, which is null whenever the
48
+ * pricing table has no rate for the route. Null renders nothing.
49
+ * No rate table, no number — kiso does not invent a price.
50
+ */
51
+ export interface StatusMeter {
52
+ readonly cacheHitPct: number | null;
53
+ readonly costUsd: number | null;
54
+ }
36
55
  /** The IDLE row: the approval tier as the CALLER names it, the /mode
37
- * hint, the model driving the session, and the ctx estimate. */
38
- export declare function idleStatus(tier: string, model: string, ctxRatio: number): string;
56
+ * hint, the model driving the session, the TUI2-R1 meter when there is
57
+ * one, and the ctx estimate. Called without a meter — or with one that
58
+ * knows nothing — the row is byte-identical to the pre-round row. */
59
+ export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter): string;
60
+ /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
61
+ * the CLI already tracks. The denominator is the TOTAL the model was
62
+ * given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
63
+ * over fresh alone once rendered 923%. No input at all → null, never a
64
+ * zero. */
65
+ export declare function cacheHitPct(usage: {
66
+ in: number | null;
67
+ cache: number | null;
68
+ }): number | null;
package/dist/status.js CHANGED
@@ -46,7 +46,28 @@ export function runningStatus(glyph, since, outTokens, ctxRatio) {
46
46
  return `${glyph} working ${seconds}s${out} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
47
47
  }
48
48
  /** The IDLE row: the approval tier as the CALLER names it, the /mode
49
- * hint, the model driving the session, and the ctx estimate. */
50
- export function idleStatus(tier, model, ctxRatio) {
51
- return `▸ ${tier} · /mode to switch · ${model} · ctx left ~${ctxLeft(ctxRatio)}%`;
49
+ * hint, the model driving the session, the TUI2-R1 meter when there is
50
+ * one, and the ctx estimate. Called without a meter — or with one that
51
+ * knows nothing the row is byte-identical to the pre-round row. */
52
+ export function idleStatus(tier, model, ctxRatio, meter) {
53
+ const parts = [`▸ ${tier}`, "/mode to switch", model];
54
+ if (meter?.cacheHitPct != null)
55
+ parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
56
+ if (meter?.costUsd != null)
57
+ parts.push(`$${meter.costUsd.toFixed(4)}`);
58
+ parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
59
+ return parts.join(" · ");
60
+ }
61
+ /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
62
+ * the CLI already tracks. The denominator is the TOTAL the model was
63
+ * given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
64
+ * over fresh alone once rendered 923%. No input at all → null, never a
65
+ * zero. */
66
+ export function cacheHitPct(usage) {
67
+ const fresh = usage.in;
68
+ const cached = usage.cache;
69
+ if (fresh === null || cached === null)
70
+ return null;
71
+ const total = fresh + cached;
72
+ return total > 0 ? (cached / total) * 100 : null;
52
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
37
  "dependencies": {
38
- "@vincemakes/kiso-tui-cells": "0.7.0"
38
+ "@vincemakes/kiso-tui-cells": "0.9.0"
39
39
  }
40
40
  }