@vincemakes/kiso-tui 0.1.39 → 0.1.41

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/render.d.ts CHANGED
@@ -6,49 +6,14 @@
6
6
  * the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
7
7
  * kiso-core's Event — the CLI translates Event → RenderInput. The tui
8
8
  * package has ZERO kiso-core imports: input is data, output is bytes.
9
+ *
10
+ * ADR-0043 Amendment 4: the cell-rendering slice (the twelve helpers
11
+ * components.ts imports) moved to tui-cells — this module re-exports
12
+ * it (compositor.ts and index.ts's imports stay verbatim) and keeps
13
+ * the event renderers (renderEvent, the status line, the recap, the
14
+ * session line).
9
15
  */
10
- /**
11
- * v2a — the palette, centralized (no hard-coded codes elsewhere); v5
12
- * (TUI v5 #16e, the v4.1 design): the decorative blue (38;5;75) is
13
- * RETIRED — the identity accents (the you> prompt, the banner tagline,
14
- * ✓ marks, slash-command names, the input brick) are bright-white BOLD
15
- * (SGR 1); the user message is the SGR-7 chip (the 2026-08-09 ruling
16
- * retired the ▍ rail); `code` is the content semantic tint for
17
- * inline code spans in assistant text (256-color 110 — the cube color
18
- * nearest the design's #8fb4d8); red for errors, dim for metadata,
19
- * green for the diff additions. NO_COLOR set, or a non-TTY output →
20
- * every code is empty, so pipes and CI carry ZERO ANSI (the existing
21
- * byte-level e2e assertions guard it). Everything not listed is plain.
22
- */
23
- export interface Palette {
24
- readonly bold: string;
25
- readonly dim: string;
26
- readonly red: string;
27
- readonly green: string;
28
- readonly code: string;
29
- readonly rv: string;
30
- readonly rvEnd: string;
31
- readonly reset: string;
32
- }
33
- export declare const COLOR_ON: Palette;
34
- export declare const COLOR_OFF: Palette;
35
- export declare function palette(): Palette;
36
- /**
37
- * E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
38
- * reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
39
- * bidi overrides. The kiso colors are applied by render, not by the data.
40
- * EVERY externally-sourced string must pass through this before any output.
41
- */
42
- export declare function escapeTerminal(text: string): string;
43
- /**
44
- * TUI v5 #16e: the inline-code tint — backtick spans in ONE line of
45
- * assistant body text get the `code` color. Deliberately NOT a markdown
46
- * engine: single level only (`[^`]*` cannot nest), a span never matches
47
- * across lines (the caller passes one line; an opener without a closer
48
- * on the same line stays plain). NO_COLOR / pipes → the codes are empty
49
- * strings → the line passes through byte-identical.
50
- */
51
- export declare function colorInlineCode(line: string): string;
16
+ export * from "@vincemakes/kiso-tui-cells/render";
52
17
  /** The canonical-path resolver for the approval detail — injected by the
53
18
  * caller (the CLI passes the tools' own resolution). The tui package is
54
19
  * pure terminal: input is data, output is bytes, zero runtime deps. */
@@ -157,35 +122,7 @@ export type RenderInput = {
157
122
  * consumer closes the segment with a newline at the next non-thinking
158
123
  * event.
159
124
  */
160
- /**
161
- * v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
162
- * " (… /think shows full)" marker when the block is longer. The consumer
163
- * buffers the block's deltas, renders this at the block's end, and keeps
164
- * the full text for /think. Pipes get the same fold — the content
165
- * strategy is presentation-independent.
166
- */
167
- export declare function foldThinking(block: string): string;
168
- /** v2b — the [result] echo truncates at 160 chars + a /last hint. */
169
- export declare function foldResult(content: string): string;
170
125
  export declare function renderEvent(ev: RenderInput, prevThinking?: boolean, resolvePath?: PathResolver): RenderResult;
171
- /**
172
- * B area: one-line summary of a completed tool call, e.g.
173
- * ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
174
- * ✗ shell npm test (exit 1)
175
- * edit/write show +/- line counts, read shows lines, shell shows the exit
176
- * code; failures (isError) are ✗. Pure and deterministic.
177
- */
178
- export declare function renderToolSummary(name: string, input: Record<string, unknown>, result: {
179
- content: string;
180
- isError: boolean;
181
- }, reason?: string | null): string;
182
- /** W15 — the expand header's target: the tool call's subject (the path
183
- * for the *_file tools, the command for shell) — the same extraction
184
- * the summary detail uses, WITHOUT the counts (the header names what
185
- * was expanded, not its size). */
186
- export declare function toolTarget(name: string, input: Record<string, unknown>): string;
187
- /** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
188
- export declare function kUnit(value: number | null): string;
189
126
  /** B area: usage data gathered from the run's usage events. */
190
127
  export interface RunUsage {
191
128
  readonly in: number | null;
@@ -202,42 +139,6 @@ export interface RunUsage {
202
139
  * passed in (chars/4 vs the window), marked with ~.
203
140
  */
204
141
  export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio: number, faux?: boolean): string | null;
205
- /**
206
- * v2a rhythm — the exact bytes after a terminal event: the status line
207
- * hugs the terminal (show what there is — omitted when there is nothing to show),
208
- * then EXACTLY one blank line before the next prompt. The consumer prints
209
- * this verbatim; the render tests pin the sequence.
210
- */
211
- export declare function renderTerminalGap(statusLine: string | null): string;
212
- /**
213
- * v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
214
- * (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
215
- * "kiso vX — tagline" + extensions. The tagline rides the version line
216
- * (the old logo MIDDLE row was the tagline — a text row masquerading as
217
- * the logo's centre). Every row truncates at the terminal width with a
218
- * " (+N)" marker (N = the hidden display width); a window narrower than
219
- * 40 columns skips the logo + the blank entirely — only the info rows.
220
- * Pure.
221
- */
222
- export declare const TAGLINE = "the coding agent that survives kill -9";
223
- /** v3 §01 (W1): truncate a row at `width`, marking the hidden span
224
- * " (+N)". W1: the width math is the charWidth authority (the banner's
225
- * brick glyphs are 1 cell — the art's 38 columns clear 40), and the
226
- * marker's own cells are part of the row — the visible cut leaves room
227
- * for it, so a truncated row never exceeds W (a cut row carries the
228
- * marker INSIDE the width; a row that fits is returned untouched). */
229
- export declare function truncateRow(row: string, width: number): string;
230
- /** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
231
- * the tier table (extends the existing "under 40 columns, skip the
232
- * logo" rule with a HEIGHT input):
233
- * W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
234
- * W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
235
- * anything smaller → text rows only
236
- * then the blank, then "vX — tagline" — the art IS the wordmark, so the
237
- * text row does not repeat the name — then extensions — then the W5
238
- * resume list (BIG only, W5). Every row truncates at the terminal width
239
- * with a " (+N)" marker. Pure. */
240
- export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
241
142
  /** v3 §02 — the recap line that ends a run, replacing the "done" label +
242
143
  * the old status line. All fields derive LOCALLY from the event stream
243
144
  * (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
@@ -247,6 +148,10 @@ export interface RecapStats {
247
148
  readonly tools: number;
248
149
  readonly edits: number;
249
150
  readonly usage: RunUsage;
151
+ /** R-C item 4: the per-turn cache miss (min(prevIn, in) − cacheRead),
152
+ * passed only when above the noise floor — the re-sent-uncached
153
+ * prefix. Absent → the recap bytes stay the historical form. */
154
+ readonly missed?: number;
250
155
  readonly ctxLeftPct: number | null;
251
156
  /** W19 — the mode the turn ran under. Under "plan" the recap becomes
252
157
  * the way-forward row (the claimed shape): a plan turn's currency is
@@ -263,19 +168,3 @@ export declare function renderSessionLine(meta: {
263
168
  runs: number;
264
169
  updatedAt: number;
265
170
  }): string;
266
- /** W5 — the opening-screen resume list. Every field already exists
267
- * behind renderSessionLine / `kiso sessions`: the relative time, the
268
- * title, then the right-aligned "N events · M runs". The columns are
269
- * fixed per W: 4 indent + 7 when + 1 + the title (the ONLY flexible
270
- * field — cut with the ellipsis marker INSIDE the width) + 1 + the meta
271
- * (padStart to metaW). The done-when: the meta's right edge lands at
272
- * exactly W on every row. Returns PLAIN rows — the banner's uniform dim
273
- * wrap styles them (no dim+bold SGR composition). */
274
- export interface ResumeMeta {
275
- readonly title: string;
276
- readonly events: number;
277
- readonly runs: number;
278
- readonly updatedAt: number;
279
- }
280
- export declare function relativeTime(updatedAt: number, now: number): string;
281
- export declare function renderResumeList(metas: readonly ResumeMeta[], W: number, now: number): string[];
package/dist/render.js CHANGED
@@ -6,41 +6,15 @@
6
6
  * the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
7
7
  * kiso-core's Event — the CLI translates Event → RenderInput. The tui
8
8
  * package has ZERO kiso-core imports: input is data, output is bytes.
9
+ *
10
+ * ADR-0043 Amendment 4: the cell-rendering slice (the twelve helpers
11
+ * components.ts imports) moved to tui-cells — this module re-exports
12
+ * it (compositor.ts and index.ts's imports stay verbatim) and keeps
13
+ * the event renderers (renderEvent, the status line, the recap, the
14
+ * session line).
9
15
  */
10
- import { charWidth, displayWidth } from "./width.js";
11
- export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;110m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
12
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
13
- export function palette() {
14
- return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
15
- }
16
- /**
17
- * E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
18
- * reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
19
- * bidi overrides. The kiso colors are applied by render, not by the data.
20
- * EVERY externally-sourced string must pass through this before any output.
21
- */
22
- export function escapeTerminal(text) {
23
- // eslint-disable-next-line no-control-regex
24
- return text
25
- .replace(/[\u0000-\u0008\u000d\u000e-\u001f\u007f]/g, "") // C0 (keeps only \t and \n)
26
- .replace(/\u001b/g, "") // ESC
27
- .replace(/[\u0080-\u009f]/g, "") // C1
28
- .replace(/[\u202a-\u202e\u2066-\u2069]/g, ""); // bidi
29
- }
30
- /**
31
- * TUI v5 #16e: the inline-code tint — backtick spans in ONE line of
32
- * assistant body text get the `code` color. Deliberately NOT a markdown
33
- * engine: single level only (`[^`]*` cannot nest), a span never matches
34
- * across lines (the caller passes one line; an opener without a closer
35
- * on the same line stays plain). NO_COLOR / pipes → the codes are empty
36
- * strings → the line passes through byte-identical.
37
- */
38
- export function colorInlineCode(line) {
39
- const p = palette();
40
- if (p.code === "" || p.reset === "")
41
- return line;
42
- return line.replace(/`([^`]*)`/g, `${p.code}\`$1\`${p.reset}`);
43
- }
16
+ import { escapeTerminal, foldResult, foldThinking, kUnit, palette } from "@vincemakes/kiso-tui-cells/render";
17
+ export * from "@vincemakes/kiso-tui-cells/render";
44
18
  /**
45
19
  * Render one event. `text` may be a continuation (text_delta appends to the
46
20
  * current line); `newline` says whether the line is complete.
@@ -50,25 +24,6 @@ export function colorInlineCode(line) {
50
24
  * consumer closes the segment with a newline at the next non-thinking
51
25
  * event.
52
26
  */
53
- /**
54
- * v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
55
- * " (… /think shows full)" marker when the block is longer. The consumer
56
- * buffers the block's deltas, renders this at the block's end, and keeps
57
- * the full text for /think. Pipes get the same fold — the content
58
- * strategy is presentation-independent.
59
- */
60
- export function foldThinking(block) {
61
- const p = palette();
62
- const trimmed = escapeTerminal(block.trim());
63
- const truncated = trimmed.length > 100;
64
- return `${p.dim}…${trimmed.slice(0, 100)}${truncated ? ` (${block.length} chars · /think)` : ""}${p.reset}\n`;
65
- }
66
- /** v2b — the [result] echo truncates at 160 chars + a /last hint. */
67
- export function foldResult(content) {
68
- const flat = content.replaceAll("\n", " ");
69
- const truncated = flat.length > 160;
70
- return `${escapeTerminal(flat.slice(0, 160))}${truncated ? " (/last for full)" : ""}`;
71
- }
72
27
  export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
73
28
  const p = palette();
74
29
  switch (ev.type) {
@@ -182,97 +137,6 @@ function approvalDetail(name, input, resolvePath) {
182
137
  }
183
138
  return `\n ${escapeTerminal(JSON.stringify(input))}`;
184
139
  }
185
- /**
186
- * B area: one-line summary of a completed tool call, e.g.
187
- * ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
188
- * ✗ shell npm test (exit 1)
189
- * edit/write show +/- line counts, read shows lines, shell shows the exit
190
- * code; failures (isError) are ✗. Pure and deterministic.
191
- */
192
- export function renderToolSummary(name, input, result, reason = null) {
193
- // v2a/v5: ✓ is a bold identity accent; ✗ stays red.
194
- const p = palette();
195
- // W19: a DENIED call (the "denied" tag) renders the pinned row — the
196
- // FULL call name, the target, the reason in the W4 parentheses idiom,
197
- // and NO timing metadata (the call never ran — (0.0s) would be noise).
198
- // The same row in the interactive and pipe paths, byte-clean on a pipe.
199
- if (reason !== null) {
200
- return `${p.red}✗${p.reset} ${escapeTerminal(`${name} ${toolTarget(name, input)} (${reason})`)}`;
201
- }
202
- const mark = result.isError ? `${p.red}✗${p.reset}` : `${p.bold}✓${p.reset}`;
203
- const shortName = name.replace("_file", "");
204
- const detail = toolSummaryDetail(name, input, result);
205
- return `${mark} ${escapeTerminal(`${shortName} ${detail}`)}`;
206
- }
207
- function toolSummaryDetail(name, input, result) {
208
- // Line count without the phantom empty line after a trailing newline.
209
- const lines = (text) => {
210
- if (text === "")
211
- return 0;
212
- const parts = text.split("\n");
213
- return parts[parts.length - 1] === "" ? parts.length - 1 : parts.length;
214
- };
215
- switch (name) {
216
- case "read_file": {
217
- const path = String(input.path ?? "?");
218
- const count = lines(String(result.content));
219
- return `${path} (${count} line${count === 1 ? "" : "s"})`;
220
- }
221
- case "write_file": {
222
- const path = String(input.path ?? "?");
223
- const count = lines(String(input.content ?? ""));
224
- return `${path} (+${count})`;
225
- }
226
- case "edit_file": {
227
- const path = String(input.path ?? "?");
228
- const removed = lines(String(input.search ?? ""));
229
- const added = lines(String(input.replace ?? ""));
230
- return `${path} (+${added} -${removed})`;
231
- }
232
- case "shell": {
233
- const command = String(input.command ?? "?");
234
- const exit = exitCodeOf(result);
235
- return `${command} (exit ${exit})`;
236
- }
237
- case "list_dir":
238
- return String(input.path ?? "(root)");
239
- default:
240
- return String(input.path ?? input.command ?? "");
241
- }
242
- }
243
- /** W15 — the expand header's target: the tool call's subject (the path
244
- * for the *_file tools, the command for shell) — the same extraction
245
- * the summary detail uses, WITHOUT the counts (the header names what
246
- * was expanded, not its size). */
247
- export function toolTarget(name, input) {
248
- switch (name) {
249
- case "read_file":
250
- case "write_file":
251
- case "edit_file":
252
- return String(input.path ?? "?");
253
- case "shell":
254
- return String(input.command ?? "?");
255
- case "list_dir":
256
- return String(input.path ?? "(root)");
257
- default:
258
- return String(input.path ?? input.command ?? "");
259
- }
260
- }
261
- /** The exit code of a shell result: parsed from the failure text, 0 on success. */
262
- function exitCodeOf(result) {
263
- if (!result.isError)
264
- return 0;
265
- const m = /exit (\d+)/.exec(result.content);
266
- return m !== null ? Number(m[1]) : 1;
267
- }
268
- /** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
269
- export function kUnit(value) {
270
- if (value === null)
271
- return "?";
272
- if (value >= 1000)
273
- return `${(value / 1000).toFixed(1).replace(/\.0$/, "")}k`;
274
- return String(value);
275
- }
276
140
  /**
277
141
  * B area/v2a: the one-line status bar after a terminal, e.g.
278
142
  * [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
@@ -299,112 +163,17 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
299
163
  return null;
300
164
  return `[turn ${turn} · ${parts.join(" · ")}]`;
301
165
  }
302
- /**
303
- * v2a rhythm — the exact bytes after a terminal event: the status line
304
- * hugs the terminal (show what there is — omitted when there is nothing to show),
305
- * then EXACTLY one blank line before the next prompt. The consumer prints
306
- * this verbatim; the render tests pin the sequence.
307
- */
308
- export function renderTerminalGap(statusLine) {
309
- return `${statusLine === null ? "" : `${statusLine}\n`}\n`;
310
- }
311
- /**
312
- * v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
313
- * (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
314
- * "kiso vX — tagline" + extensions. The tagline rides the version line
315
- * (the old logo MIDDLE row was the tagline — a text row masquerading as
316
- * the logo's centre). Every row truncates at the terminal width with a
317
- * " (+N)" marker (N = the hidden display width); a window narrower than
318
- * 40 columns skips the logo + the blank entirely — only the info rows.
319
- * Pure.
320
- */
321
- export const TAGLINE = "the coding agent that survives kill -9";
322
- /** v6's existing logo — W1's COMPACT tier, byte-identical, no redraw. */
323
- const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
324
- /** W1's BIG tier (36x6, `█` and space only — no half-blocks, so there is
325
- * no tile seam to lose in a font that renders ▀ ▄ at the wrong height).
326
- * Each pixel is two cells wide on purpose (a terminal cell is ~1:2);
327
- * the render indents two — 38 columns total, clears 40. */
328
- const BIG_LOGO_ROWS = [
329
- "██ ██ ██████ ████████ ████████",
330
- "██ ██ ██ ██ ██ ██",
331
- "████ ██ ████████ ██ ██",
332
- "████ ██ ██ ██ ██",
333
- "██ ██ ██ ██ ██ ██",
334
- "██ ██ ██████ ████████ ████████",
335
- ];
336
- /** v3 §01 (W1): truncate a row at `width`, marking the hidden span
337
- * " (+N)". W1: the width math is the charWidth authority (the banner's
338
- * brick glyphs are 1 cell — the art's 38 columns clear 40), and the
339
- * marker's own cells are part of the row — the visible cut leaves room
340
- * for it, so a truncated row never exceeds W (a cut row carries the
341
- * marker INSIDE the width; a row that fits is returned untouched). */
342
- export function truncateRow(row, width) {
343
- const total = displayWidth(row);
344
- if (total <= width)
345
- return row;
346
- // iterate the marker to a fixpoint: the marker's width changes the
347
- // cut, the cut changes the hidden count the marker reports
348
- let marker = " (+0)";
349
- for (;;) {
350
- const cut = Math.max(0, width - displayWidth(marker));
351
- let w = 0;
352
- let i = 0;
353
- while (i < row.length) {
354
- const cp = row.codePointAt(i);
355
- const cw = charWidth(cp);
356
- if (w + cw > cut)
357
- break;
358
- w += cw;
359
- i += cp > 0xffff ? 2 : 1; // code-point stepping — never split a pair
360
- }
361
- const next = ` (+${total - w})`;
362
- if (next === marker)
363
- return `${row.slice(0, i)}${next}`;
364
- marker = next;
365
- }
366
- }
367
- /** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
368
- * the tier table (extends the existing "under 40 columns, skip the
369
- * logo" rule with a HEIGHT input):
370
- * W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
371
- * W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
372
- * anything smaller → text rows only
373
- * then the blank, then "vX — tagline" — the art IS the wordmark, so the
374
- * text row does not repeat the name — then extensions — then the W5
375
- * resume list (BIG only, W5). Every row truncates at the terminal width
376
- * with a " (+N)" marker. Pure. */
377
- export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
378
- const rows = [];
379
- if (W >= 40) {
380
- if (H >= 20) {
381
- for (const r of BIG_LOGO_ROWS)
382
- rows.push(truncateRow(` ${r}`, W));
383
- }
384
- else if (H >= 14) {
385
- for (const r of LOGO_ROWS)
386
- rows.push(truncateRow(r, W));
387
- }
388
- }
389
- if (rows.length > 0)
390
- rows.push("");
391
- rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
392
- if (extensionsText !== "")
393
- rows.push(truncateRow(extensionsText, W));
394
- if (W >= 40 && H >= 20 && resume.length > 0) {
395
- rows.push("", ...renderResumeList(resume, W, now));
396
- }
397
- return rows;
398
- }
399
166
  export function renderRecap(s) {
400
167
  const p = palette();
401
168
  // W19: under plan the recap is the way out of the mode — the header
402
169
  // names the mode's posture, the hints name the exits (the ONLY
403
- // controls — /mode is the only way to leave plan mode).
170
+ // controls — /mode is the only way to leave plan mode). The plan
171
+ // branch drops the metadata segments (ctx left) BEFORE the fold: the
172
+ // fixed hints are 79 cols — ONE row at W=80, never the folded line —
173
+ // and the ctx-left hint lives where it always lived: the status
174
+ // row's right side.
404
175
  if (s.mode === "plan") {
405
176
  const parts = ["plan ready", "/mode default executes", "/mode accept-edits auto-approves edits"];
406
- if (s.ctxLeftPct !== null)
407
- parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
408
177
  return `${p.bold}▞${p.reset} ${parts.join(" · ")}\n`;
409
178
  }
410
179
  const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
@@ -413,7 +182,8 @@ export function renderRecap(s) {
413
182
  if (seg !== "")
414
183
  parts.push(seg);
415
184
  if (s.usage.cache !== null && s.usage.in !== null && s.usage.in > 0) {
416
- parts.push(`cache ${Math.round((s.usage.cache / s.usage.in) * 100)}%`);
185
+ const hit = `cache ${Math.round((s.usage.cache / s.usage.in) * 100)}%`;
186
+ parts.push(s.missed !== undefined && s.missed > 0 ? `${hit} · miss ${kUnit(s.missed)}` : hit);
417
187
  }
418
188
  }
419
189
  if (s.ctxLeftPct !== null)
@@ -426,50 +196,3 @@ export function renderSessionLine(meta) {
426
196
  // round 8: the title is the user's first prompt — model/user text, escaped.
427
197
  return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
428
198
  }
429
- export function relativeTime(updatedAt, now) {
430
- const s = Math.max(0, now - updatedAt) / 1000;
431
- if (s < 60)
432
- return "now";
433
- const m = Math.floor(s / 60);
434
- if (m < 60)
435
- return `${m}m ago`;
436
- const h = Math.floor(m / 60);
437
- if (h < 24)
438
- return `${h}h ago`;
439
- const d = Math.floor(h / 24);
440
- if (d < 7)
441
- return `${d}d ago`;
442
- return `${Math.floor(d / 7)}w ago`;
443
- }
444
- function titleCut(text, max) {
445
- if (displayWidth(text) <= max)
446
- return text;
447
- const room = max - displayWidth("…");
448
- let w = 0;
449
- let i = 0;
450
- while (i < text.length) {
451
- const cp = text.codePointAt(i);
452
- const cw = charWidth(cp);
453
- if (w + cw > room)
454
- break;
455
- w += cw;
456
- i += cp > 0xffff ? 2 : 1;
457
- }
458
- return text.slice(0, i) + "…";
459
- }
460
- export function renderResumeList(metas, W, now) {
461
- if (metas.length === 0)
462
- return [];
463
- const rows = [" ▞ resume"];
464
- const whens = metas.map((m) => relativeTime(m.updatedAt, now));
465
- const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
466
- const metaW = Math.max(...metaTexts.map((t) => t.length));
467
- const titleW = Math.max(1, W - 13 - metaW);
468
- for (let i = 0; i < metas.length; i += 1) {
469
- const title = escapeTerminal(metas[i].title);
470
- const shown = titleCut(title, titleW);
471
- const pad = titleW - displayWidth(shown);
472
- rows.push(` ${whens[i].padEnd(7)} ${shown}${" ".repeat(pad)} ${metaTexts[i].padStart(metaW)}`);
473
- }
474
- return rows;
475
- }
package/dist/width.d.ts CHANGED
@@ -1,15 +1,4 @@
1
- /**
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).
9
- */
10
- /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
- export declare function charWidth(cp: number): number;
12
- /** Display width of a code-point array (cursor math, scrolling). */
13
- export declare function widthOf(chars: readonly number[]): number;
14
- /** Display width of a string. */
15
- export declare function displayWidth(text: string): number;
1
+ /** The display-width primitives moved to tui-cells (ADR-0043
2
+ * Amendment 4) this module is the re-export shim: the tui's
3
+ * internal imports (./width.js) stay verbatim. */
4
+ export * from "@vincemakes/kiso-tui-cells/width";
package/dist/width.js CHANGED
@@ -1,59 +1,4 @@
1
- /**
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).
9
- */
10
- /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
- export function charWidth(cp) {
12
- if (cp >= 0x1100 && cp <= 0x115f)
13
- return 2; // hangul jamo
14
- if (cp >= 0x2e80 && cp <= 0x303e)
15
- return 2; // radicals .. CJK punctuation
16
- if (cp >= 0x3041 && cp <= 0x33ff)
17
- return 2; // kana, CJK compat
18
- if (cp >= 0x3400 && cp <= 0x4dbf)
19
- return 2; // CJK ext A
20
- if (cp >= 0x4e00 && cp <= 0x9fff)
21
- return 2; // CJK unified
22
- if (cp >= 0xa000 && cp <= 0xa4cf)
23
- return 2; // yi
24
- if (cp >= 0xa960 && cp <= 0xa97f)
25
- return 2; // hangul jamo ext
26
- if (cp >= 0xac00 && cp <= 0xd7a3)
27
- return 2; // hangul syllables
28
- if (cp >= 0xf900 && cp <= 0xfaff)
29
- return 2; // CJK compat ideographs
30
- if (cp >= 0xfe10 && cp <= 0xfe19)
31
- return 2; // vertical forms
32
- if (cp >= 0xfe30 && cp <= 0xfe6f)
33
- return 2; // CJK compat forms
34
- if (cp >= 0xff00 && cp <= 0xff60)
35
- return 2; // fullwidth forms
36
- if (cp >= 0xffe0 && cp <= 0xffe6)
37
- return 2; // fullwidth signs
38
- if (cp >= 0x1f300 && cp <= 0x1f64f)
39
- return 2; // emoji (misc + emoticons)
40
- if (cp >= 0x1f900 && cp <= 0x1f9ff)
41
- return 2; // supplemental emoji
42
- if (cp >= 0x20000 && cp <= 0x3fffd)
43
- return 2; // CJK ext B..G
44
- return 1;
45
- }
46
- /** Display width of a code-point array (cursor math, scrolling). */
47
- export function widthOf(chars) {
48
- let w = 0;
49
- for (const cp of chars)
50
- w += charWidth(cp);
51
- return w;
52
- }
53
- /** Display width of a string. */
54
- export function displayWidth(text) {
55
- let w = 0;
56
- for (const ch of text)
57
- w += charWidth(ch.codePointAt(0));
58
- return w;
59
- }
1
+ /** The display-width primitives moved to tui-cells (ADR-0043
2
+ * Amendment 4) this module is the re-export shim: the tui's
3
+ * internal imports (./width.js) stay verbatim. */
4
+ export * from "@vincemakes/kiso-tui-cells/width";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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",
@@ -33,5 +33,8 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/vincemakes/kiso/issues"
35
35
  },
36
- "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme"
36
+ "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
+ "dependencies": {
38
+ "@vincemakes/kiso-tui-cells": "0.1.41"
39
+ }
37
40
  }