@vincemakes/kiso-tui-cells 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.
@@ -75,6 +75,38 @@ export interface AskRuntime {
75
75
  readonly custom: readonly (string | null)[];
76
76
  readonly phase: "options" | "custom";
77
77
  }
78
+ /** One thing that can be picked: what it is, and what qualifies it. */
79
+ export interface PickOption {
80
+ readonly label: string;
81
+ /** the dim qualifier ("profile: ds \u00b7 current") \u2014 what tells two
82
+ * similar rows apart */
83
+ readonly note?: string;
84
+ }
85
+ /** The whole pick: the header sentence, the options, the free-text
86
+ * escape hatch, and the honest empty state. */
87
+ export interface PickSpec {
88
+ readonly header: string;
89
+ readonly options: readonly PickOption[];
90
+ /** the `t` row \u2014 typing it directly is always available, because a
91
+ * list of profiles is never the list of models that exist */
92
+ readonly typeHint: string;
93
+ /** shown INSTEAD of the options when there are none. The copy is the
94
+ * caller's and is reproduced verbatim. */
95
+ readonly emptyNote?: string;
96
+ }
97
+ /** The pick panel's runtime state \u2014 the editor owns it, the compositor
98
+ * reads it (the AskRuntime precedent, two fields instead of five). */
99
+ export interface PickRuntime {
100
+ readonly cursor: number;
101
+ readonly phase: "options" | "custom";
102
+ }
103
+ /** What was picked: a listed option by INDEX (never a label the caller
104
+ * would have to re-match against its own list), or typed text. */
105
+ export type PickResult = {
106
+ readonly index: number;
107
+ } | {
108
+ readonly custom: string;
109
+ };
78
110
  /** The ALWAYS-verbose args (the panel's body): the untruncated diff
79
111
  * (edit/write), or the full text (shell = the command line, other =
80
112
  * the pretty-printed JSON). The CLI composes them UNTRUNCATED — the
@@ -116,6 +148,9 @@ export interface PanelView {
116
148
  * panel renders the ask block and the editor routes the ask keys;
117
149
  * absent = the approval/simple panel, unchanged. */
118
150
  readonly ask?: AskSpec;
151
+ /** TUI2-R2 \u2463: the options, when this view is a PICK. Same contract
152
+ * as `ask`, one payload over. */
153
+ readonly pick?: PickSpec;
119
154
  }
120
155
  export type PanelVerdict = {
121
156
  readonly action: "allow";
@@ -135,6 +170,13 @@ export type PanelVerdict = {
135
170
  | {
136
171
  readonly action: "answers";
137
172
  readonly result: AskResult;
173
+ }
174
+ /** TUI2-R2 \u2463: the pick's verdict \u2014 the chosen index or the typed
175
+ * text. Only pick views ever produce it, so the approval path's
176
+ * switch is untouched. */
177
+ | {
178
+ readonly action: "picked";
179
+ readonly result: PickResult;
138
180
  };
139
181
  /** The bound panel state the compositor reads — the editor owns the
140
182
  * phase/selection state machine and the key routing; the compositor
@@ -145,6 +187,8 @@ export interface PanelState {
145
187
  readonly sel: PanelSel;
146
188
  /** KC3.5: the ask's walk — present exactly when `view.ask` is. */
147
189
  readonly ask?: AskRuntime;
190
+ /** TUI2-R2 \u2463: the pick's walk — present exactly when `view.pick` is. */
191
+ readonly pick?: PickRuntime;
148
192
  }
149
193
  /** The block's rows — EXACTLY the preview's frame shape, the gutter at
150
194
  * the left edge (the preview's two-space mock indent is its own
@@ -168,3 +212,34 @@ export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: Pan
168
212
  * phase's keys. The approval flavor gains the tab-amend path; the
169
213
  * simple flavor (the trust/uncertain gates) never does. */
170
214
  export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
215
+ /**
216
+ * The pick block's rows — the prototype's C frame.
217
+ *
218
+ * The header says what is in effect right now, because the first
219
+ * question anyone opening this panel has is "what am I on?". The
220
+ * options are numbered from 1 and the number IS the key. The `t` row is
221
+ * last and always present: a profile list is a convenience, never the
222
+ * set of models that exist, and a picker that can only offer what
223
+ * someone remembered to configure is a smaller product than the one it
224
+ * replaced.
225
+ *
226
+ * Single-row discipline: every row CUTS, never folds — the block's
227
+ * height is its row count (the W20 rule the #checked throw demands).
228
+ */
229
+ export declare function pickBlockRows(view: PanelView, state: PickRuntime, W: number, maxRows: number): string[];
230
+ /** The digits are the keys, so the list the panel offers is bounded by
231
+ * the digits there are. Beyond it, `/model <name>` still takes any
232
+ * profile — the panel says so rather than paginating. */
233
+ export declare const PICK_MAX = 9;
234
+ /** The input row's lead: the digit range while picking, the named
235
+ * prompt while typing one out. */
236
+ export declare function pickLeadPlain(view: PanelView, state: PickRuntime): string;
237
+ export declare function pickLead(view: PanelView, state: PickRuntime): string;
238
+ /** The status row's left text \u2014 the CALLER's, because only the caller
239
+ * knows whether a run is paused behind this panel. */
240
+ export declare function pickStatus(view: PanelView): string;
241
+ export declare function pickAffordance(state: PickRuntime): string;
242
+ /** Compose a pick view. The flavor/name/title/args fields exist for the
243
+ * approval path and are given inert values here \u2014 the pick block
244
+ * reads none of them. */
245
+ export declare function modelPickView(spec: PickSpec, statusText: string): PanelView;
@@ -26,6 +26,9 @@
26
26
  */
27
27
  import { displayWidth } from "./width.js";
28
28
  import { cutLine, diffBody, gutterFold, visibleWidth, widthCut } from "./components.js";
29
+ // TUI2-R2pre ④: strings.js takes only a TYPE from this module, so the
30
+ // import is erased at compile time and no runtime cycle exists.
31
+ import { displayVerb } from "./strings.js";
29
32
  import { escapeTerminal, palette } from "./render.js";
30
33
  /** The rule line's text — the why-asked line (the R3 chain): the tool
31
34
  * name, the first non-abstain speaker, the fix hint (the §3.5 table,
@@ -37,7 +40,11 @@ function panelRuleText(view) {
37
40
  if (view.ruleOverride !== undefined)
38
41
  return escapeTerminal(view.ruleOverride);
39
42
  const hint = view.hint;
40
- const base = `${p.bold}${escapeTerminal(view.name)}${p.reset} ${p.dim}needs approvalasked by${p.reset} ${p.bold}${escapeTerminal(view.speaker)}${p.reset}`;
43
+ // TUI2-R2pre ④: the rule line is the panel's header it says the ACT
44
+ // ("edit needs approval"). view.name keeps the RAW tool name, which is
45
+ // what the option-2 rule prefill and the fallbackQuestion (the
46
+ // dock-less/pipe path — byte-identical by ruling) still read.
47
+ const base = `${p.bold}${escapeTerminal(displayVerb(view.name))}${p.reset} ${p.dim}needs approval — asked by${p.reset} ${p.bold}${escapeTerminal(view.speaker)}${p.reset}`;
41
48
  return hint ? `${base}${p.dim} ·${p.reset} ${p.code}${escapeTerminal(hint)}${p.reset}` : base;
42
49
  }
43
50
  /** The numbered options row — "1 Yes 2 Yes, don't ask again for
@@ -170,3 +177,88 @@ export function panelAffordance(view, phase, sel) {
170
177
  return sel === 0 ? "tab amend · esc cancel" : "enter sends · esc backs out";
171
178
  return sel === 0 ? "esc cancel" : "enter sends";
172
179
  }
180
+ // ── TUI2-R2 ④: the pick block, its lead, its status, its affordance ──
181
+ /**
182
+ * The pick block's rows — the prototype's C frame.
183
+ *
184
+ * The header says what is in effect right now, because the first
185
+ * question anyone opening this panel has is "what am I on?". The
186
+ * options are numbered from 1 and the number IS the key. The `t` row is
187
+ * last and always present: a profile list is a convenience, never the
188
+ * set of models that exist, and a picker that can only offer what
189
+ * someone remembered to configure is a smaller product than the one it
190
+ * replaced.
191
+ *
192
+ * Single-row discipline: every row CUTS, never folds — the block's
193
+ * height is its row count (the W20 rule the #checked throw demands).
194
+ */
195
+ export function pickBlockRows(view, state, W, maxRows) {
196
+ const p = palette();
197
+ const spec = view.pick;
198
+ const gutter = `${p.dim}\u2502${p.reset} `;
199
+ const rows = [];
200
+ const room = Math.max(1, W - 2);
201
+ rows.push(`${gutter}${cutLine(`${p.bold}${escapeTerminal(spec.header.split(" \u2014 ")[0] ?? spec.header)}${p.reset}${p.dim}${escapeTerminal(spec.header.slice((spec.header.split(" \u2014 ")[0] ?? "").length))}${p.reset}`, room)}`);
202
+ if (spec.options.length === 0) {
203
+ // the honest empty state \u2014 the caller's own copy, verbatim
204
+ rows.push(`${gutter}${cutLine(`${p.dim} ${escapeTerminal(spec.emptyNote ?? "no options")}${p.reset}`, room)}`);
205
+ }
206
+ else {
207
+ // the budget: the header, the t row, the affordance and the rule
208
+ const budget = Math.max(1, maxRows - 4);
209
+ const shown = spec.options.slice(0, Math.min(budget, PICK_MAX));
210
+ for (let i = 0; i < shown.length; i += 1) {
211
+ const o = shown[i];
212
+ const mark = i === state.cursor && state.phase === "options";
213
+ const head = `${mark ? p.bold : ""} ${i + 1} ${escapeTerminal(o.label)}${mark ? p.reset : ""}`;
214
+ const note = o.note === undefined ? "" : `${p.dim} ${escapeTerminal(o.note)}${p.reset}`;
215
+ rows.push(`${gutter}${cutLine(`${head}${note}`, room)}`);
216
+ }
217
+ if (spec.options.length > shown.length) {
218
+ rows.push(`${gutter}${cutLine(`${p.dim} \u2514 +${spec.options.length - shown.length} more \u2014 /model <name> takes any of them${p.reset}`, room)}`);
219
+ }
220
+ }
221
+ rows.push(`${gutter}${cutLine(`${state.phase === "custom" ? p.bold : ""} t ${p.reset}${p.dim}${escapeTerminal(spec.typeHint)}${p.reset}`, room)}`);
222
+ rows.push(`${gutter}${p.dim}${cutLine(pickAffordance(state), room)}${p.reset}`);
223
+ rows.push(`${p.dim}\u2514${"\u2500".repeat(Math.max(0, W - 1))}${p.reset}`);
224
+ return rows;
225
+ }
226
+ /** The digits are the keys, so the list the panel offers is bounded by
227
+ * the digits there are. Beyond it, `/model <name>` still takes any
228
+ * profile — the panel says so rather than paginating. */
229
+ export const PICK_MAX = 9;
230
+ /** The input row's lead: the digit range while picking, the named
231
+ * prompt while typing one out. */
232
+ export function pickLeadPlain(view, state) {
233
+ if (state.phase === "custom")
234
+ return "provider/model: ";
235
+ const n = Math.min(view.pick.options.length, PICK_MAX);
236
+ return n === 0 ? "t> " : `1-${n}> `;
237
+ }
238
+ export function pickLead(view, state) {
239
+ const p = palette();
240
+ return `${p.bold}${pickLeadPlain(view, state)}${p.reset}`;
241
+ }
242
+ /** The status row's left text \u2014 the CALLER's, because only the caller
243
+ * knows whether a run is paused behind this panel. */
244
+ export function pickStatus(view) {
245
+ return view.statusText;
246
+ }
247
+ export function pickAffordance(state) {
248
+ return state.phase === "custom" ? "enter commits \u00b7 esc backs out" : "digits pick \u00b7 \u23ce confirms \u00b7 esc";
249
+ }
250
+ /** Compose a pick view. The flavor/name/title/args fields exist for the
251
+ * approval path and are given inert values here \u2014 the pick block
252
+ * reads none of them. */
253
+ export function modelPickView(spec, statusText) {
254
+ return {
255
+ flavor: "simple",
256
+ name: "model",
257
+ title: "model",
258
+ speaker: "you",
259
+ statusText,
260
+ args: { kind: "text", lines: [] },
261
+ fallbackQuestion: "switch model? (name) ",
262
+ pick: spec,
263
+ };
264
+ }
@@ -215,12 +215,37 @@ export declare function gutterFold(gutter: string, line: string, W: number): str
215
215
  * ellipsis ride the row (the invariant ① cap holds). */
216
216
  export declare function gutterCut(gutter: string, line: string, W: number): string[];
217
217
  export declare function expandSuffix(lines: number | null, room: number): string;
218
+ /**
219
+ * TUI2-R2 ⑤ (D, candidate 1) — the FOCUS tint.
220
+ *
221
+ * The cell the next ctrl+r will act on brightens its own `ctrl+r` token
222
+ * to the code tint; the rest of the suffix — the separator, the count —
223
+ * stays dim, because what is being marked is the KEY's target, not the
224
+ * row. Zero new rows, zero new columns: the affordance the cell already
225
+ * prints is the marker.
226
+ *
227
+ * Applied to a row rather than composed into it on purpose. The token is
228
+ * emitted from several places (the settled suffix, the renderer's own
229
+ * `└ +N … · ctrl+r` cut rows) and threading a flag through all of them
230
+ * would put the invariant "exactly one bright token" in as many hands as
231
+ * there are emitters. Here it has exactly one.
232
+ *
233
+ * NO_COLOR: p.code is empty, so the row's bytes are untouched.
234
+ */
235
+ export declare function focusToken(row: string, W: number): string;
218
236
  /** W13 — the rollup opt-in table: which tools collapse, and the count
219
237
  * NOUN (read_file calls → "5 files", list_dir → "5 dirs", search_text
220
238
  * → "5 matches"). Only these tools opt in — a shell burst is never
221
239
  * rolled up (its rows carry meaning). The folded-turn line (W14) reuses
222
240
  * the plurals for its other-tool terms ("2 dirs", "1 match"). */
223
241
  export declare const ROLLUP_NOUN: Readonly<Record<string, string>>;
242
+ /** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
243
+ * TUI2-R2pre ④: this used to be a private three-tool table saying the
244
+ * same thing as the card head's `_file` strip, in a different way and
245
+ * for a different set of tools. Both are `displayVerb` now — the whole
246
+ * point of the ruling is that there is ONE answer to "what does the
247
+ * screen call this". The cut note, which used to be the deliberate
248
+ * exception here, moved with it (see toolCutNote). */
224
249
  /** Whether a tool joins an exploration run. Exactly the read-only set —
225
250
  * writes, edits, shells and extension tools never group (a burst of
226
251
  * side effects is a list of things that HAPPENED, and every row of it
@@ -18,6 +18,10 @@
18
18
  * tint, fold wording).
19
19
  */
20
20
  import { displayWidth } from "./width.js";
21
+ // TUI2-R2pre ④: the ONE display-verb table (strings.ts, beside
22
+ // KEY_BINDINGS). strings.js imports only render/width here, so this edge
23
+ // adds no cycle.
24
+ import { displayVerb } from "./strings.js";
21
25
  import { bannerLines, escapeTerminal, foldThinking, foldResult, colorInlineCode, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, } from "./render.js";
22
26
  /** The spinner glyphs, cycled by the compositor's on-demand tick. */
23
27
  export const SPINNER = ["▖", "▘", "▝", "▗"];
@@ -454,7 +458,7 @@ class ToolExecution {
454
458
  render(W, ctx) {
455
459
  const p = palette();
456
460
  const c = this.cell;
457
- const verb = escapeTerminal(c.name.replace("_file", ""));
461
+ const verb = escapeTerminal(displayVerb(c.name));
458
462
  const verbCol = verb.length < 5 ? `${verb}${" ".repeat(5 - verb.length)}` : verb;
459
463
  const parts = c.rolled?.parts;
460
464
  if (c.rolled !== null && parts !== undefined) {
@@ -711,6 +715,46 @@ function appendSuffix(row, suffix) {
711
715
  const p = palette();
712
716
  return `${row}${p.dim}${suffix}${p.reset}`;
713
717
  }
718
+ /**
719
+ * TUI2-R2 ⑤ (D, candidate 1) — the FOCUS tint.
720
+ *
721
+ * The cell the next ctrl+r will act on brightens its own `ctrl+r` token
722
+ * to the code tint; the rest of the suffix — the separator, the count —
723
+ * stays dim, because what is being marked is the KEY's target, not the
724
+ * row. Zero new rows, zero new columns: the affordance the cell already
725
+ * prints is the marker.
726
+ *
727
+ * Applied to a row rather than composed into it on purpose. The token is
728
+ * emitted from several places (the settled suffix, the renderer's own
729
+ * `└ +N … · ctrl+r` cut rows) and threading a flag through all of them
730
+ * would put the invariant "exactly one bright token" in as many hands as
731
+ * there are emitters. Here it has exactly one.
732
+ *
733
+ * NO_COLOR: p.code is empty, so the row's bytes are untouched.
734
+ */
735
+ export function focusToken(row, W) {
736
+ const p = palette();
737
+ const at = row.lastIndexOf(CTRL_R);
738
+ if (at !== -1) {
739
+ // the row already names the key — brighten the token in place, and
740
+ // leave every other span exactly as it was
741
+ if (p.code === "")
742
+ return row;
743
+ return `${row.slice(0, at)}${p.code}${CTRL_R}${p.reset}${p.dim}${row.slice(at + CTRL_R.length)}`;
744
+ }
745
+ // A LIVE row does not carry the affordance today, and the live cell is
746
+ // the one ctrl+r takes FIRST (expandNext scans the live tail before
747
+ // the committed ring) — so the row the key is aimed at was the one row
748
+ // that never said the key existed. The affordance IS the marker here:
749
+ // it appears on the focused row and nowhere else, which is why no
750
+ // unfocused row's bytes move (every existing live-row assertion
751
+ // renders a cell with no focus and is untouched).
752
+ const room = W - visibleWidth(row);
753
+ if (room < SUFFIX_MIN)
754
+ return row; // never at the cost of invariant ①
755
+ return `${row}${p.dim} · ${p.reset}${p.code}${CTRL_R}${p.reset}`;
756
+ }
757
+ const CTRL_R = "ctrl+r";
714
758
  /** TUI2-R1 (A) — the expanded block's last row: the way back. The
715
759
  * rollup's expanded list carries a second clause (its members' full
716
760
  * outputs live in /last, which the group row cannot show). */
@@ -736,10 +780,13 @@ const EXPLORE_NOUN = {
736
780
  list_dir: ["dir", "dirs"],
737
781
  search_text: ["search", "searches"],
738
782
  };
739
- /** TUI2-R1 (B) — the verb column of the expanded list. `search_text`
740
- * reads as "search" there: the column names the ACT, and the raw tool
741
- * name is what the cut notes carry (they name what the model calls). */
742
- const EXPLORE_VERB = { read_file: "read", list_dir: "list", search_text: "search" };
783
+ /** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
784
+ * TUI2-R2pre ④: this used to be a private three-tool table saying the
785
+ * same thing as the card head's `_file` strip, in a different way and
786
+ * for a different set of tools. Both are `displayVerb` now — the whole
787
+ * point of the ruling is that there is ONE answer to "what does the
788
+ * screen call this". The cut note, which used to be the deliberate
789
+ * exception here, moved with it (see toolCutNote). */
743
790
  /** Whether a tool joins an exploration run. Exactly the read-only set —
744
791
  * writes, edits, shells and extension tools never group (a burst of
745
792
  * side effects is a list of things that HAPPENED, and every row of it
@@ -770,7 +817,7 @@ export function exploreRows(parts, W) {
770
817
  counts.set(s, (counts.get(s) ?? 0) + 1);
771
818
  const shown = [...counts.entries()].slice(0, 3).map(([s, n]) => (n > 1 ? `${s} ×${n}` : s));
772
819
  const more = counts.size > 3 ? ` (+${counts.size - 3})` : "";
773
- const verb = EXPLORE_VERB[part.name] ?? part.name;
820
+ const verb = displayVerb(part.name);
774
821
  rows.push(cutLine(`${p.dim}${BODY_ROW}${escapeTerminal(`${verb.padEnd(6)} ${shown.join(" · ")}${more}`)}${p.reset}`, W));
775
822
  }
776
823
  // TUI2-R1.5 ① (VD-15): the footer used to promise "/last shows the full
@@ -812,7 +859,7 @@ export function turnFold(t, W) {
812
859
  parts.push(countTerm(n, noun.endsWith("es") ? noun.slice(0, -2) : noun.slice(0, -1), noun));
813
860
  }
814
861
  else {
815
- const verb = name.replace("_file", "");
862
+ const verb = displayVerb(name);
816
863
  parts.push(countTerm(n, verb, `${verb}s`));
817
864
  }
818
865
  }
@@ -1110,14 +1157,22 @@ export function diffBody(diff, W, expanded = false) {
1110
1157
  * offset=N", the output cap, list_dir's entry cap). The note reaches
1111
1158
  * the MODEL and never the human — this row surfaces it. Detected in
1112
1159
  * the result's TAIL (the note is appended at the end); returns null
1113
- * when the tool did not truncate. */
1160
+ * when the tool did not truncate.
1161
+ *
1162
+ * TUI2-R2pre ④: the verb here is the DISPLAY one now. This row used to
1163
+ * be the sanctioned raw-name exception, on the reasoning that it names
1164
+ * the tool the model should call again — but the row is addressed to
1165
+ * the HUMAN (the model already has the note in its own transcript, which
1166
+ * is where it read it), and the ruling names this advisory family
1167
+ * explicitly. The `offset=N` it carries is the actionable half and is
1168
+ * untouched. */
1114
1169
  function toolCutNote(name, resultText) {
1115
1170
  const tail = resultText.slice(-300);
1116
1171
  const m = /offset=(\d+)/.exec(tail);
1117
1172
  if (m !== null)
1118
- return `capped by ${escapeTerminal(name)} · offset=${m[1]} for the rest`;
1173
+ return `capped by ${escapeTerminal(displayVerb(name))} · offset=${m[1]} for the rest`;
1119
1174
  if (/…\[truncated\]/.test(tail) || /… \+?\d+ more (?:lines|entries)/.test(tail))
1120
- return `capped by ${escapeTerminal(name)} · /last for the rest`;
1175
+ return `capped by ${escapeTerminal(displayVerb(name))} · /last for the rest`;
1121
1176
  return null;
1122
1177
  }
1123
1178
  /** The assistant body text — wrapped at W, the inline-code tint per
package/dist/index.d.ts CHANGED
@@ -6,11 +6,12 @@
6
6
  * package); the cli never imports it directly. Experimental — no
7
7
  * API-stability promise yet.
8
8
  */
9
- export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
9
+ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
10
10
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
11
11
  export { pendingQueueRows } from "./components.js";
12
12
  export { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
13
13
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, } from "./approval-panel.js";
14
14
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact, } from "./strings.js";
15
15
  export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
16
+ export { displayVerb } from "./strings.js";
16
17
  export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * package); the cli never imports it directly. Experimental — no
7
7
  * API-stability promise yet.
8
8
  */
9
- export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
9
+ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
10
10
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
11
11
  // W22 (the v8 input round): the pending-queue chips — the SAME
12
12
  // UserMessage chip with the □ gutter, pre-rendered above the input
@@ -25,4 +25,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
25
25
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, } from "./strings.js";
26
26
  // KC3.5: the interrupted-ask copy and the extracted /help table.
27
27
  export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
28
+ // TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
29
+ // the tool table names the call.
30
+ export { displayVerb } from "./strings.js";
28
31
  export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
package/dist/render.d.ts CHANGED
@@ -34,6 +34,14 @@ export interface Palette {
34
34
  readonly dim: string;
35
35
  readonly red: string;
36
36
  readonly green: string;
37
+ /** TUI2-R2 ①: the third functional exception, finally spelled. The
38
+ * mono-discipline ruling above names "green ✓, yellow warn and red
39
+ * error" as the ONLY functional colours; warn had no entry because
40
+ * nothing had needed it yet. The uncertain badge needs exactly it —
41
+ * a state that is neither success nor failure but a question
42
+ * addressed to the human. This is the ruling's own set gaining its
43
+ * missing member, not a fourth colour. */
44
+ readonly warn: string;
37
45
  readonly code: string;
38
46
  readonly rv: string;
39
47
  readonly rvEnd: string;
package/dist/render.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * dependencies (the tui-cells package has none).
7
7
  */
8
8
  import { charWidth, displayWidth } from "./width.js";
9
- export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;252m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
10
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
9
+ export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", warn: "\x1b[33m", code: "\x1b[38;5;252m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
10
+ export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", rv: "", rvEnd: "", reset: "" };
11
11
  export function palette() {
12
12
  return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
13
13
  }
package/dist/strings.d.ts CHANGED
@@ -111,6 +111,10 @@ export interface KeyBinding {
111
111
  * then the completions.
112
112
  */
113
113
  export declare const KEY_BINDINGS: readonly KeyBinding[];
114
+ /** A tool's name as the SCREEN says it. Display-only: the raw name stays
115
+ * on the cell, and dispatch, the mode gate, the policy keys, the /last
116
+ * RAW block and every model-facing byte keep reading that. */
117
+ export declare function displayVerb(name: string): string;
114
118
  /** The panel keys, which belong to a panel rather than the composer —
115
119
  * one dim line rather than four table rows, because they apply only
116
120
  * while a panel is up. */
package/dist/strings.js CHANGED
@@ -155,6 +155,40 @@ export const KEY_BINDINGS = [
155
155
  { keys: "tab", what: "complete (menu / @)" },
156
156
  { keys: "?", what: "this sheet" },
157
157
  ];
158
+ /**
159
+ * TUI2-R2pre ④ — THE display-verb table (the integrator's ruling).
160
+ *
161
+ * The screen names the ACT; the tool table names the CALL. Two
162
+ * audiences, two vocabularies, and only the human's one lives here: the
163
+ * API names DO NOT change, because the model-request surface is frozen
164
+ * rent and every byte of it is paid for on every turn. The
165
+ * rename-the-tools path is REJECTED by ruling.
166
+ *
167
+ * One table, for the same reason KEY_BINDINGS above is one table. The
168
+ * mapping used to exist three and a half times — a `.replace("_file",
169
+ * "")` in components.ts, another in render.ts, two more in the
170
+ * compositor, and a private three-tool table for the rollup's expanded
171
+ * list — and the drift was visible on a single screen: a card head
172
+ * reading `read` directly above one reading `list_dir`.
173
+ *
174
+ * An unmapped tool (an extension's, an MCP server's) renders its own
175
+ * name. Inventing a verb for a tool this package has never heard of
176
+ * would be a worse lie than printing what the model actually calls.
177
+ */
178
+ const DISPLAY_VERB = {
179
+ read_file: "read",
180
+ list_dir: "list",
181
+ search_text: "search",
182
+ write_file: "write",
183
+ edit_file: "edit",
184
+ shell: "shell",
185
+ };
186
+ /** A tool's name as the SCREEN says it. Display-only: the raw name stays
187
+ * on the cell, and dispatch, the mode gate, the policy keys, the /last
188
+ * RAW block and every model-facing byte keep reading that. */
189
+ export function displayVerb(name) {
190
+ return DISPLAY_VERB[name] ?? name;
191
+ }
158
192
  /** The panel keys, which belong to a panel rather than the composer —
159
193
  * one dim line rather than four table rows, because they apply only
160
194
  * while a panel is up. */
package/dist/width.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  /**
2
2
  * The display-width primitives — the SINGLE width authority (TUI v5
3
- * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
- * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
- * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
- * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
- * ZWJ clusters are not guaranteed perfect — each code point counts as
8
- * its width. Zero dependencies (importable from any module).
3
+ * #16e: "charWidth is the width authority"). The table covers the East
4
+ * Asian Wide/Fullwidth ranges and Emoji_Presentation=Yes; everything
5
+ * else is one column, the box-drawing/brick glyphs █▀▄▞▸ and the text-
6
+ * presentation marks included.
7
+ *
8
+ * This is the compositor's FLOOR, not a cosmetic detail: a glyph scored
9
+ * one column that a terminal draws in two makes a line whose measured
10
+ * width is <= W really need W+1, the terminal soft-wraps the tail onto
11
+ * the row below, and the live region silently eats a row it never
12
+ * budgeted (TUI2-R2pre ① — the composer clobber).
13
+ *
14
+ * Known limitation, documented in the README: emoji ZWJ clusters and
15
+ * variation-selector sequences are not guaranteed perfect — each code
16
+ * point counts as its own width (U+26A0 + FE0F sums to 2, which is what
17
+ * a terminal draws, but that is arithmetic luck, not a model).
18
+ * Zero dependencies (importable from any module).
9
19
  */
10
20
  /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
21
  export declare function charWidth(cp: number): number;
package/dist/width.js CHANGED
@@ -1,12 +1,63 @@
1
1
  /**
2
2
  * The display-width primitives — the SINGLE width authority (TUI v5
3
- * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
- * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
- * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
- * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
- * ZWJ clusters are not guaranteed perfect — each code point counts as
8
- * its width. Zero dependencies (importable from any module).
3
+ * #16e: "charWidth is the width authority"). The table covers the East
4
+ * Asian Wide/Fullwidth ranges and Emoji_Presentation=Yes; everything
5
+ * else is one column, the box-drawing/brick glyphs █▀▄▞▸ and the text-
6
+ * presentation marks included.
7
+ *
8
+ * This is the compositor's FLOOR, not a cosmetic detail: a glyph scored
9
+ * one column that a terminal draws in two makes a line whose measured
10
+ * width is <= W really need W+1, the terminal soft-wraps the tail onto
11
+ * the row below, and the live region silently eats a row it never
12
+ * budgeted (TUI2-R2pre ① — the composer clobber).
13
+ *
14
+ * Known limitation, documented in the README: emoji ZWJ clusters and
15
+ * variation-selector sequences are not guaranteed perfect — each code
16
+ * point counts as its own width (U+26A0 + FE0F sums to 2, which is what
17
+ * a terminal draws, but that is arithmetic luck, not a model).
18
+ * Zero dependencies (importable from any module).
9
19
  */
20
+ /** TUI2-R2pre ① — the Emoji_Presentation=Yes code points inside
21
+ * U+2000..U+2BFF. The rest of that span is TEXT presentation and stays
22
+ * one column: ✓ ✗ ⚠ ⏸ ▞ ▸ and the box-drawing rails are all narrow, and
23
+ * widening any of them would move every card head on the screen. Listed
24
+ * as ranges because that is what the property is — the singles are
25
+ * singles in Unicode too. */
26
+ const EMOJI_PRESENTATION = [
27
+ [0x231a, 0x231b],
28
+ [0x23e9, 0x23ec],
29
+ [0x23f0, 0x23f0],
30
+ [0x23f3, 0x23f3],
31
+ [0x25fd, 0x25fe],
32
+ [0x2614, 0x2615],
33
+ [0x2648, 0x2653],
34
+ [0x267f, 0x267f],
35
+ [0x2693, 0x2693],
36
+ [0x26a1, 0x26a1],
37
+ [0x26aa, 0x26ab],
38
+ [0x26bd, 0x26be],
39
+ [0x26c4, 0x26c5],
40
+ [0x26ce, 0x26ce],
41
+ [0x26d4, 0x26d4],
42
+ [0x26ea, 0x26ea],
43
+ [0x26f2, 0x26f3],
44
+ [0x26f5, 0x26f5],
45
+ [0x26fa, 0x26fa],
46
+ [0x26fd, 0x26fd],
47
+ [0x2705, 0x2705],
48
+ [0x270a, 0x270b],
49
+ [0x2728, 0x2728],
50
+ [0x274c, 0x274c],
51
+ [0x274e, 0x274e],
52
+ [0x2753, 0x2755],
53
+ [0x2757, 0x2757],
54
+ [0x2795, 0x2797],
55
+ [0x27b0, 0x27b0],
56
+ [0x27bf, 0x27bf],
57
+ [0x2b1b, 0x2b1c],
58
+ [0x2b50, 0x2b50],
59
+ [0x2b55, 0x2b55],
60
+ ];
10
61
  /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
62
  export function charWidth(cp) {
12
63
  if (cp >= 0x1100 && cp <= 0x115f)
@@ -35,12 +86,33 @@ export function charWidth(cp) {
35
86
  return 2; // fullwidth forms
36
87
  if (cp >= 0xffe0 && cp <= 0xffe6)
37
88
  return 2; // fullwidth signs
89
+ // TUI2-R1.5 shipped only two of the pictographic ranges; the holes
90
+ // (transport, mahjong/cards, enclosed, colored shapes, the extended
91
+ // block) were scored ONE column while every terminal draws them in
92
+ // two — the composer clobber of the owner's field report (①).
93
+ if (cp === 0x1f004 || cp === 0x1f0cf)
94
+ return 2; // mahjong red dragon, joker
95
+ if (cp >= 0x1f18e && cp <= 0x1f19a)
96
+ return 2; // enclosed alphanumerics
97
+ if (cp >= 0x1f200 && cp <= 0x1f251)
98
+ return 2; // enclosed ideographic
38
99
  if (cp >= 0x1f300 && cp <= 0x1f64f)
39
100
  return 2; // emoji (misc + emoticons)
101
+ if (cp >= 0x1f680 && cp <= 0x1f6ff)
102
+ return 2; // transport + map
103
+ if (cp >= 0x1f7e0 && cp <= 0x1f7eb)
104
+ return 2; // colored circles + squares
40
105
  if (cp >= 0x1f900 && cp <= 0x1f9ff)
41
106
  return 2; // supplemental emoji
107
+ if (cp >= 0x1fa70 && cp <= 0x1faff)
108
+ return 2; // symbols + pictographs ext-A
42
109
  if (cp >= 0x20000 && cp <= 0x3fffd)
43
110
  return 2; // CJK ext B..G
111
+ if (cp >= 0x231a && cp <= 0x2b55) {
112
+ for (const [lo, hi] of EMOJI_PRESENTATION)
113
+ if (cp >= lo && cp <= hi)
114
+ return 2;
115
+ }
44
116
  return 1;
45
117
  }
46
118
  /** Display width of a code-point array (cursor math, scrolling). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",