@vincemakes/kiso-tui-cells 0.25.0 → 0.26.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/index.d.ts 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, focusToken, ROLLUP_NOUN, foldTerms, 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, foldTerms, 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";
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, focusToken, ROLLUP_NOUN, foldTerms, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
9
+ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, foldTerms, 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
package/dist/render.d.ts CHANGED
@@ -88,23 +88,6 @@ export interface Palette {
88
88
  * spans and must end without stranding them. */
89
89
  readonly wash: string;
90
90
  readonly washEnd: string;
91
- /** R7a — THE FOCUS MARKER'S EMPHASIS, and it is not a background.
92
- *
93
- * DC-3 gave the `ctrl+o` token the wash, which is a BACKGROUND once
94
- * a ground is resolved: `48;5;236` on dark reads as a black block
95
- * behind the key, on a row that is otherwise plain text. The owner
96
- * asked for it gone. The invariant DC-3 was serving — exactly one
97
- * bright token per frame, because the key has exactly one target —
98
- * never required a background; it required CONTRAST against the dim
99
- * siblings, and full-strength bold foreground is more of it than a
100
- * wash was.
101
- *
102
- * Three escapes because "dim" has two forms here: SGR 2 in the
103
- * neutral palette, a 256-colour foreground in the resolved ones.
104
- * 22 cancels the attribute, 39 restores the default foreground, 1
105
- * is the emphasis. It closes by re-opening the palette's own dim,
106
- * like `washEnd`, so the surrounding span survives. */
107
- readonly lift: string;
108
91
  /** R9 P3 — THE ONE GREY ALLOWED ON THE WASH.
109
92
  *
110
93
  * §2.1 bars `dim` from the wash and the measurement is why: `#767676`
@@ -248,7 +231,20 @@ export interface BannerMeta {
248
231
  readonly model: string;
249
232
  readonly mode: string;
250
233
  readonly cwd: string;
234
+ /** DC-49 — the workspace IS the user's home directory. Computed by the
235
+ * CLI (realpath on both sides); the banner only renders it. */
236
+ readonly homeWorkspace?: boolean;
251
237
  }
238
+ /** W20 — the ONE-ROW cut with the honest mark, SGR-aware. A line that
239
+ * fits (≤ W) passes through whole; an overflow cuts the content at
240
+ * W−1 — the ellipsis's slot — and the ellipsis rides AFTER the reset
241
+ * (post-reset — the PTY needles' convention). The cut row never
242
+ * exceeds W (invariant ①). One implementation for every one-row
243
+ * surface: the live task rows, the approval panel's lines (W21), the
244
+ * help and keys rows, the transcript viewer's rows. (The strings module
245
+ * and the viewer each carried a copy; the viewer's put the ellipsis
246
+ * before the reset, and now does not.) */
247
+ export declare function cutLine(line: string, W: number): string;
252
248
  /** v3 §01 (W1): truncate a row at `width`, marking the hidden span
253
249
  * " (+N)". W1: the width math is the charWidth authority (the banner's
254
250
  * brick glyphs are 1 cell — the art's 38 columns clear 40), and the
package/dist/render.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * produce the bytes a human sees. Colors are raw ANSI — zero
6
6
  * dependencies (the tui-cells package has none).
7
7
  */
8
- import { charWidth, displayWidth } from "./width.js";
8
+ import { charWidth, displayWidth, visibleWidth, widthCut } from "./width.js";
9
9
  const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", warn: "\x1b[33m", italic: "\x1b[3m", italicEnd: "\x1b[23m", underline: "\x1b[4m", underlineEnd: "\x1b[24m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
10
10
  /**
11
11
  * DC-9 (design §2.3) — the failure colour is theme-resolved.
@@ -40,7 +40,6 @@ const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim, washDim = "") =
40
40
  washDim,
41
41
  washDimEnd: washDim === "" ? "" : "\x1b[39m",
42
42
  code: wash,
43
- lift: "\x1b[22m\x1b[39m\x1b[1m",
44
43
  });
45
44
  /**
46
45
  * DC-3 — one table per ground.
@@ -69,7 +68,7 @@ export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m", "\x1b[38;5;173m
69
68
  * not been established. Unchanged in every byte except `code`, which
70
69
  * was the defect. */
71
70
  export const COLOR_ON = COLOR_NEUTRAL;
72
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", washDim: "", washDimEnd: "", lift: "", reset: "" };
71
+ export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", washDim: "", washDimEnd: "", reset: "" };
73
72
  /** DC-3 — the resolved ground, set once at startup when the terminal
74
73
  * answers (see `ground.ts`). It starts UNKNOWN and may stay that way
75
74
  * forever; that is a supported state, not a failure. */
@@ -292,20 +291,6 @@ export function twinkleFrame(step) {
292
291
  /** Both cycles are seven frames, so ONE counter walks them and the two
293
292
  * marks stay in step on a screen showing both. */
294
293
  export const MOTION_FRAMES = 7;
295
- /** DC-18: the display-width prefix of PLAIN text. `widthCut` lives in
296
- * components.ts, which imports this module — the dependency runs one
297
- * way, so the four lines live here rather than inverting it. */
298
- function plainCut(text, max) {
299
- let w = 0;
300
- let i = 0;
301
- for (; i < text.length; i += 1) {
302
- const cw = charWidth(text.codePointAt(i));
303
- if (w + cw > max)
304
- break;
305
- w += cw;
306
- }
307
- return text.slice(0, i);
308
- }
309
294
  export const TAGLINE = "the coding agent that survives kill -9";
310
295
  /**
311
296
  * R2 — the wordmark is retired (2026-08-27, the nineteen-screen review).
@@ -335,6 +320,45 @@ const BANNER_KEYS = "esc interrupt · ctrl+c exit · / commands · @ files · ?
335
320
  * rather than by SGR: they mark sections and are never content. */
336
321
  const BANNER_LABELS = ["MODEL", "WORKSPACE", "EXTENSIONS"];
337
322
  const LABEL_STOP = Math.max(...BANNER_LABELS.map((l) => l.length)) + 2;
323
+ /** W20 — the ONE-ROW cut with the honest mark, SGR-aware. A line that
324
+ * fits (≤ W) passes through whole; an overflow cuts the content at
325
+ * W−1 — the ellipsis's slot — and the ellipsis rides AFTER the reset
326
+ * (post-reset — the PTY needles' convention). The cut row never
327
+ * exceeds W (invariant ①). One implementation for every one-row
328
+ * surface: the live task rows, the approval panel's lines (W21), the
329
+ * help and keys rows, the transcript viewer's rows. (The strings module
330
+ * and the viewer each carried a copy; the viewer's put the ellipsis
331
+ * before the reset, and now does not.) */
332
+ export function cutLine(line, W) {
333
+ if (visibleWidth(line) <= W)
334
+ return line;
335
+ let out = "";
336
+ let width = 0;
337
+ for (let i = 0; i < line.length;) {
338
+ if (line[i] === "\x1b") {
339
+ // exec returns an ARRAY — copying m coerces it (the match), but
340
+ // m.length is the CAPTURE count (1), not the sequence length:
341
+ // the old `i += m.length` re-processed the sequence's bracket
342
+ // text as literal rows, doubling every code in a cut line
343
+ // (the W21 panel-slot red test). Index 0 is the sequence.
344
+ const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i))?.[0] ?? line[i];
345
+ out += m;
346
+ i += m.length;
347
+ continue;
348
+ }
349
+ const cw = displayWidth(line[i]);
350
+ if (width + cw > W - 1)
351
+ break; // reserve the ellipsis's column
352
+ out += line[i];
353
+ width += cw;
354
+ i += 1;
355
+ }
356
+ // R8a: the reset comes from the PALETTE, not hardcoded. `\x1b[0m`
357
+ // here put an escape into every cut row under NO_COLOR and behind a
358
+ // pipe — the one context COLOR_OFF exists to keep clean (§1.2). A
359
+ // coloured palette is byte-identical, because its reset IS `\x1b[0m`.
360
+ return `${out}${palette().reset}…`;
361
+ }
338
362
  /** v3 §01 (W1): truncate a row at `width`, marking the hidden span
339
363
  * " (+N)". W1: the width math is the charWidth authority (the banner's
340
364
  * brick glyphs are 1 cell — the art's 38 columns clear 40), and the
@@ -351,7 +375,7 @@ export function truncateRow(row, width) {
351
375
  // terminal — and invariant ① throws rather than truncating. A marker
352
376
  // wider than the row it marks is not a marker.
353
377
  if (width < 7)
354
- return plainCut(row, Math.max(0, width));
378
+ return widthCut(row, Math.max(0, width));
355
379
  // iterate the marker to a fixpoint: the marker's width changes the
356
380
  // cut, the cut changes the hidden count the marker reports
357
381
  let marker = " (+0)";
@@ -397,13 +421,24 @@ export function bannerLines(W, H, version, extensionsText, resume = [], now = Da
397
421
  // measured 11 cells and invariant ① threw AT STARTUP — the function
398
422
  // whose own comment preaches "invariant ① holds at every width".
399
423
  // The cut is taken on the plain text, per the note above.
400
- const namePlain = plainCut(`kiso ${version}`, Math.max(1, W));
424
+ const namePlain = widthCut(`kiso ${version}`, Math.max(1, W));
401
425
  const nameCut = namePlain.slice(0, 4); // "kiso", or its surviving prefix
402
426
  const verCut = namePlain.slice(5); // the version, if the width left room for it
403
427
  const rows = [`${p.bold}${nameCut}${p.reset}${verCut === "" ? "" : `${p.dim} ${verCut}${p.reset}`}`];
404
428
  const facts = [];
405
429
  if (meta !== undefined) {
406
430
  facts.push([BANNER_LABELS[0], `${meta.model}${meta.mode === "" ? "" : ` · ${meta.mode}`}`], [BANNER_LABELS[1], meta.cwd]);
431
+ // DC-49 — ONE row under the cwd, and only when the workspace is the
432
+ // home directory. It STATES a fact and names the remedy; it does not
433
+ // warn, because the configuration is ALLOWED (owner, 2026-09-06) and
434
+ // a warning about an allowed thing teaches people to skip rows.
435
+ //
436
+ // An empty label puts it in the value column under `WORKSPACE`,
437
+ // where it reads as a note on that fact rather than a fact of its
438
+ // own. It must FIT at W=80 under the label indent: a cut row loses
439
+ // the remedy, which is the only actionable half of the sentence.
440
+ if (meta.homeWorkspace === true)
441
+ facts.push(["", "home directory as workspace — cd into a project to narrow it"]);
407
442
  }
408
443
  if (extensionsText !== "")
409
444
  facts.push([BANNER_LABELS[2], extensionsText]);
package/dist/strings.js CHANGED
@@ -17,8 +17,8 @@
17
17
  * why the caller passes paths and counts in rather than having them
18
18
  * looked up here.
19
19
  */
20
- import { escapeTerminal, palette } from "./render.js";
21
- import { displayWidth, visibleWidth } from "./width.js";
20
+ import { cutLine, escapeTerminal, palette } from "./render.js";
21
+ import { displayWidth } from "./width.js";
22
22
  /** v2a: the interactive prompt — the identity accent. readline owns the
23
23
  * echo of what the user types; we own the prompt's color. (v2c: the
24
24
  * readline prompt keeps "you> " — the brick ▌ is the dock's row only;
@@ -196,6 +196,14 @@ export const KEY_BINDINGS = [
196
196
  { keys: "ctrl+r", what: "transcript" },
197
197
  { keys: "tab", what: "complete (menu / @)" },
198
198
  { keys: "?", what: "this sheet" },
199
+ // E1 §1/§3 — the editor's daily three. Three spellings of the word
200
+ // gestures reach the same code (alt, ctrl+arrow, and alt+b/f where
201
+ // the terminal sends meta); the sheet names the two a reader is most
202
+ // likely to have. ctrl+w is listed beside them because it works on
203
+ // every terminal, including the ones that send none of the three.
204
+ { keys: "alt+←→ / ctrl+←→", what: "word motion" },
205
+ { keys: "alt+⌫ / alt+d", what: "delete word (ctrl+w too)" },
206
+ { keys: "ctrl+x", what: "copy the last answer" },
199
207
  { keys: "ctrl+z / ctrl+y", what: "undo / redo" },
200
208
  ];
201
209
  /**
@@ -279,7 +287,14 @@ const SHEET_GRID = [
279
287
  [8, 9],
280
288
  // UD-1's undo row shares this one now — the table has an even count
281
289
  // again, so no binding needs a row to itself.
290
+ //
291
+ // E1 §1/§3 — three more, and the warning above earned itself a second
292
+ // time: adding them to KEY_BINDINGS without touching this grid pushed
293
+ // the last three off the sheet, and the "ONE SOURCE" gate caught it
294
+ // exactly as its comment predicted it would.
282
295
  [10, 11],
296
+ [12, 13],
297
+ [14],
283
298
  ];
284
299
  const SHEET_STOPS = [
285
300
  [16, 43],
@@ -287,6 +302,18 @@ const SHEET_STOPS = [
287
302
  [36],
288
303
  [36],
289
304
  [36],
305
+ // E1 §1/§3 — the two-column rows the three new bindings land on.
306
+ // A THIRD table that must agree with the other two and nothing makes
307
+ // it: `SHEET_GRID` says which bindings share a row, this says where
308
+ // their second column starts, and `KEY_BINDINGS` says what they are.
309
+ // The grid's own comment warned about the pair; the trio is worse.
310
+ // The stop is 39 here because `alt+⌫ / alt+d delete word (ctrl+w too)`
311
+ // is the widest first cell on the sheet — a narrower stop packs the
312
+ // two cells together with a single space and the column disappears.
313
+ // The last row has ONE binding, so it pads nothing (an empty stop
314
+ // list, not a stop of 36, which would leave trailing blanks).
315
+ [39],
316
+ [],
290
317
  ];
291
318
  /**
292
319
  * TUI2-R1 (D) — the sheet, one screen, static.
@@ -355,7 +382,7 @@ export function keysSheetRows(W) {
355
382
  rows.push(row);
356
383
  }
357
384
  rows.push(`${p.dim}${panelKeysRow(W)}${p.reset}`);
358
- return rows.map((row) => cutRow(row, W));
385
+ return rows.map((row) => cutLine(row, W));
359
386
  }
360
387
  /**
361
388
  * DC-2 — the panel row degrades by CLAUSE.
@@ -364,7 +391,7 @@ export function keysSheetRows(W) {
364
391
  * used to lose the tail of the last one, so `t types` became `t`: the
365
392
  * reader was told a key existed and not told what it did, on a row that
366
393
  * still looked complete. Dropping a whole clause says less; it never
367
- * says something false. `cutRow`'s ellipsis is the floor below this, for
394
+ * says something false. `cutLine`'s ellipsis is the floor below this, for
368
395
  * a width that cannot hold even the first clause.
369
396
  */
370
397
  function panelKeysRow(W) {
@@ -376,36 +403,6 @@ function panelKeysRow(W) {
376
403
  }
377
404
  return clauses[0];
378
405
  }
379
- /** One row, cut at the width — SGR-aware, the ellipsis after the reset
380
- * (the cutLine convention; duplicated here rather than imported so the
381
- * strings module keeps its no-components-dependency shape). */
382
- function cutRow(row, W) {
383
- // DC-2: a cut is MARKED. This returned the surviving prefix with
384
- // nothing to say it was a prefix, so a row that had lost its tail read
385
- // as a whole row — the one thing the tree's own fold rule forbids
386
- // ("the honest …, never a silent truncate"). The mark costs a column,
387
- // so the cut lands one column earlier to pay for it.
388
- if (displayWidth(row.replace(/\x1b\[[0-9;]*m/g, "")) <= W)
389
- return row;
390
- const limit = Math.max(0, W - 1);
391
- let out = "";
392
- let width = 0;
393
- for (let i = 0; i < row.length;) {
394
- if (row[i] === "\x1b") {
395
- const m = /^\x1b\[[0-9;]*m/.exec(row.slice(i))?.[0] ?? row[i];
396
- out += m;
397
- i += m.length;
398
- continue;
399
- }
400
- const cw = displayWidth(row[i]);
401
- if (width + cw > limit)
402
- break;
403
- out += row[i];
404
- width += cw;
405
- i += 1;
406
- }
407
- return `${out}${palette().reset}\u2026`;
408
- }
409
406
  /** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
410
407
  * sheet renders, joined — so the two can disagree only by deleting a
411
408
  * test. The sheet is the readable form; this is the greppable one. */
@@ -436,6 +433,7 @@ export function helpRows() {
436
433
  // R4 (C4d): a committed row is the terminal's, and cannot be
437
434
  // re-wrapped in place (ADR-0046) — this appends it re-folded.
438
435
  ["/rewrap", "re-print the recent prose at the current width"],
436
+ ["/copy", "copy the last answer (raw markdown) — ctrl+x does the same"],
439
437
  ["/status", "show session id, event count, and context estimate"],
440
438
  ["/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"],
441
439
  ["/model", "list model profiles; /model <name|provider/model> switches"],
package/dist/width.d.ts CHANGED
@@ -28,6 +28,14 @@ export declare function breakable(cp: number): boolean;
28
28
  export declare function widthOf(chars: readonly number[]): number;
29
29
  /** Display width of a string. */
30
30
  export declare function displayWidth(text: string): number;
31
+ /** The display-width prefix of a text — the cell cut, no mark. Steps
32
+ * by CODE POINT, so an astral character (an emoji) counts its true
33
+ * width and is never split between its surrogates: the cut lands
34
+ * before it when it does not fit. SGR-blind — the caller cuts what it
35
+ * has already measured as visible text. The one cutter under
36
+ * `cutLine` (the marked one-row cut), the banner's plain rows (DC-18)
37
+ * and the approval panel's option-2 rule name (W21). */
38
+ export declare function widthCut(text: string, max: number): string;
31
39
  /** The visible width of a RENDERED line — the same table, asked with
32
40
  * the SGR/CSI sequences skipped. The compositor's invariant ① measures
33
41
  * with this, so every producer of a screen row must measure with it
package/dist/width.js CHANGED
@@ -151,6 +151,26 @@ export function displayWidth(text) {
151
151
  w += charWidth(ch.codePointAt(0));
152
152
  return w;
153
153
  }
154
+ /** The display-width prefix of a text — the cell cut, no mark. Steps
155
+ * by CODE POINT, so an astral character (an emoji) counts its true
156
+ * width and is never split between its surrogates: the cut lands
157
+ * before it when it does not fit. SGR-blind — the caller cuts what it
158
+ * has already measured as visible text. The one cutter under
159
+ * `cutLine` (the marked one-row cut), the banner's plain rows (DC-18)
160
+ * and the approval panel's option-2 rule name (W21). */
161
+ export function widthCut(text, max) {
162
+ let w = 0;
163
+ let i = 0;
164
+ while (i < text.length) {
165
+ const cp = text.codePointAt(i);
166
+ const cw = charWidth(cp);
167
+ if (w + cw > max)
168
+ break;
169
+ w += cw;
170
+ i += cp > 0xffff ? 2 : 1;
171
+ }
172
+ return text.slice(0, i);
173
+ }
154
174
  /** The visible width of a RENDERED line — the same table, asked with
155
175
  * the SGR/CSI sequences skipped. The compositor's invariant ① measures
156
176
  * with this, so every producer of a screen row must measure with it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.25.0",
3
+ "version": "0.26.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",