@vincemakes/kiso-tui-cells 0.19.0 → 0.20.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.
@@ -32,6 +32,18 @@ export interface FrameCtx {
32
32
  * the tier table reads H, so a resize RE-TIERS instead of
33
33
  * re-folding frozen rows). */
34
34
  readonly height: number;
35
+ /** R7a — this cell is drawn UNDER an activity header that carries the
36
+ * breathing mark, so its own head row wears a plain gutter.
37
+ *
38
+ * The mark belongs to the ACTIVITY, not to each call in it. A
39
+ * four-file burst drew four breathing marks, which is four marks
40
+ * distinguishing nothing (law 1.3, the same ground R2 retired the
41
+ * tick and cross on) — and worse, on a read that finishes in
42
+ * 200ms the mark is gone before the eye lands, so per-row it is
43
+ * motion that never resolves into meaning. On the header it is lit
44
+ * for the whole stretch, which is the fact it is there to carry:
45
+ * work is in flight. Owner-ruled 2026-08-31. */
46
+ readonly grouped?: boolean;
35
47
  }
36
48
  /** ONE screen line a component emits (raw, SGR included). */
37
49
  export type RenderLine = string;
@@ -392,7 +404,11 @@ export interface StretchTerms {
392
404
  */
393
405
  export declare function stretchLine(t: StretchTerms & {
394
406
  readonly phase: "thinking" | "acting" | "settled";
407
+ readonly mark?: string;
395
408
  }, W: number): string[];
409
+ /** R6/D3: the quiet turn's fold wears no mark either — the SECOND
410
+ * emission site, and the one the D3 brief did not name. Same ruling,
411
+ * same two-space indent; see stretchLine above for the argument. */
396
412
  export declare function turnFold(t: {
397
413
  words: string;
398
414
  thoughtSeconds: number;
@@ -149,7 +149,10 @@ export function cellComponent(cell) {
149
149
  case "user":
150
150
  return new UserMessage(cell);
151
151
  case "thinking":
152
- return new ThinkingFold(cell);
152
+ // R7: the committed/live surface is the BLOCK. ThinkingFold
153
+ // survives for the pipe path (render.ts's foldThinking), whose
154
+ // bytes are asserted by the --plain identity gate.
155
+ return new ThinkingBlock(cell);
153
156
  case "tool":
154
157
  return new ToolExecution(cell);
155
158
  case "md":
@@ -281,6 +284,57 @@ export function pendingQueueRows(lines, W) {
281
284
  * CJK — 2 cells per char — and tripped invariant ① on a real
282
285
  * Chinese session). W2: the leading ⋯ is the thinking gutter — the
283
286
  * midline mark (the state), never the text ellipsis (the truncation). */
287
+ /**
288
+ * R7 — THINKING IS A BLOCK OF WORDS.
289
+ *
290
+ * The owner's ruling, arrived at from a side-by-side with pi's screen:
291
+ * the model's reasoning reads as its own paragraphs — italic, dim,
292
+ * indented two — and is never folded away. `ThinkingFold` (below, kept
293
+ * for the pipe path) summarised it to one row with a key; four rounds
294
+ * of machinery were then built to hand the rest back, and the owner's
295
+ * complaint through all of them was the same sentence: I cannot see
296
+ * what it was thinking.
297
+ *
298
+ * THE INDENT IS NOT DECORATION. Law 1.2 is SETTLED — "strip every
299
+ * escape sequence and no fact is lost" — and italic is SGR 3, which a
300
+ * pipe strips and `COLOR_OFF` empties outright. Stripped, an italic
301
+ * paragraph and the model's ANSWER are the same bytes, and the reader
302
+ * cannot tell the reasoning from the reply. The two-space indent is the
303
+ * byte that survives: reasoning sits in, the answer stands at the
304
+ * margin. The owner chose this over the un-indented form for exactly
305
+ * that reason, with the cost stated.
306
+ *
307
+ * Paragraphs are preserved (a blank line between them); the newlines
308
+ * INSIDE a paragraph collapse, because a hard-wrapped source line is
309
+ * the model's line width, not the reader's.
310
+ */
311
+ class ThinkingBlock {
312
+ cell;
313
+ constructor(cell) {
314
+ this.cell = cell;
315
+ }
316
+ render(W, _ctx) {
317
+ const p = palette();
318
+ const text = escapeTerminal(this.cell.text).trim();
319
+ if (text === "")
320
+ return [];
321
+ const room = Math.max(1, W - 2);
322
+ const rows = [];
323
+ for (const para of text.split(/\n\s*\n/)) {
324
+ const flat = para.replace(/\s+/g, " ").trim();
325
+ if (flat === "")
326
+ continue;
327
+ if (rows.length > 0)
328
+ rows.push("");
329
+ // foldLine is the ONE width authority and returns real rows —
330
+ // invariant ①b (a row is one physical row) holds by
331
+ // construction rather than by remembering to split.
332
+ for (const line of foldLine(flat, room))
333
+ rows.push(` ${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
334
+ }
335
+ return rows;
336
+ }
337
+ }
284
338
  class ThinkingFold {
285
339
  cell;
286
340
  constructor(cell) {
@@ -687,7 +741,7 @@ class ToolExecution {
687
741
  // cannot be predicted: a turning mark implies progress the
688
742
  // product does not have. With no ground the breath freezes to a
689
743
  // static `●` and says the same thing more quietly.
690
- const out = gutterCut(`${breathFrame(ctx.spinnerI)} `, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
744
+ const out = gutterCut(ctx.grouped === true ? " " : `${breathFrame(ctx.spinnerI)} `, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
691
745
  out[0] = `${out[0]}${p.dim}${dur}${p.reset}`;
692
746
  out.push(...toolBlockBody(c, W));
693
747
  return out;
@@ -873,7 +927,7 @@ export function focusToken(row, W) {
873
927
  // attribute like bold is spent everywhere. It closes with washEnd
874
928
  // rather than a reset, so the surrounding dim survives instead of
875
929
  // having to be re-applied.
876
- return `${row.slice(0, at)}${p.wash}${CTRL_R}${p.washEnd}${row.slice(at + CTRL_R.length)}`;
930
+ return `${row.slice(0, at)}${p.lift}${CTRL_R}${p.dim}${row.slice(at + CTRL_R.length)}`;
877
931
  }
878
932
  // A LIVE row does not carry the affordance today, and the live cell is
879
933
  // the one ctrl+r takes FIRST (expandNext scans the live tail before
@@ -885,7 +939,7 @@ export function focusToken(row, W) {
885
939
  const room = W - visibleWidth(row);
886
940
  if (room < SUFFIX_MIN)
887
941
  return row; // never at the cost of invariant ①
888
- return `${row}${p.dim} · ${p.reset}${p.wash}${CTRL_R}${p.washEnd}`;
942
+ return `${row}${p.dim} · ${p.lift}${CTRL_R}${p.reset}`;
889
943
  }
890
944
  const CTRL_R = "ctrl+r";
891
945
  /** TUI2-R1 (A) — the expanded block's last row: the way back. The
@@ -1248,7 +1302,40 @@ function troubleClause(t) {
1248
1302
  export function stretchLine(t, W) {
1249
1303
  const p = palette();
1250
1304
  const live = t.phase !== "settled";
1251
- const mark = t.phase === "settled" ? `${p.bold}✦${p.reset}` : `${p.dim}${t.mark ?? "✧"}${p.reset}`;
1305
+ // DECLARED SUPERSESSION (R6/D3, owner-ruled) THE STRETCH LINE WEARS
1306
+ // NO MARK, in any phase.
1307
+ //
1308
+ // Law 1.3: a symbol earns its cell by carrying a fact the words do
1309
+ // not. When every settled fold, the live line AND the status row all
1310
+ // wear a star, none of them distinguishes anything — it is the tick
1311
+ // and the cross again (R2 retired those on exactly this ground), at
1312
+ // the stretch scale. design.md §7.4 had already ruled the principle
1313
+ // one scale down: "only the call still running carries a mark,
1314
+ // because only it is moving", with a settled call's mark "(none) —
1315
+ // the outcome is in the words. SETTLED." A settled STRETCH wearing
1316
+ // one contradicted a precedent the file had already ratified.
1317
+ //
1318
+ // Nothing settled is being reversed: §4 lists this mark PROPOSED and
1319
+ // §8 lists it OPEN. This is that proposal's ruling arriving, as a
1320
+ // decline, on the owner's own dogfood.
1321
+ //
1322
+ // The replacement is a two-space INDENT, not a column shift: the row
1323
+ // joins the settled-call family's geometry, the indent survives a
1324
+ // pipe as bytes (prose never starts at column 3), and both forms are
1325
+ // 2 cells so the width ladder below does not reflow. The status row's
1326
+ // twinkle survives as the ONE moving mark; `✦ took` survives as the
1327
+ // turn's seal.
1328
+ //
1329
+ // R7a AMENDS this by ONE case: the live ACTING line takes the
1330
+ // breathing mark, passed in. D3 declined a mark on the SETTLED fold,
1331
+ // where the words already carry the outcome; a line that means "work
1332
+ // is in flight RIGHT NOW" carries a fact its words do not, which is
1333
+ // exactly the test law 1.3 sets. It is also where the mark was
1334
+ // migrating TO: §7.4's "only the call still running carries one" now
1335
+ // applies at the stretch scale, one mark for the activity instead of
1336
+ // one per call. Owner-ruled 2026-08-31, on the ground that a fast
1337
+ // call's per-row mark is gone before the eye lands.
1338
+ const mark = t.mark ?? " ";
1252
1339
  // R4a (owner ruling, 2026-08-30) — the fold row prints NO key.
1253
1340
  //
1254
1341
  // R4 printed `· ctrl+r 3` so the row could name its own target. The
@@ -1290,6 +1377,9 @@ export function stretchLine(t, W) {
1290
1377
  const row = `${mark}${words === "" ? "" : ` ${p.rv}${words}${p.rvEnd}${p.dim} ·${p.reset}`} ${meta}${clause === "" ? "" : `${p.red}${clause}${p.reset}`}${key === "" ? "" : `${p.dim}${key}${p.reset}`}`;
1291
1378
  return [visibleWidth(row) <= W ? row : cutLine(row, W)];
1292
1379
  }
1380
+ /** R6/D3: the quiet turn's fold wears no mark either — the SECOND
1381
+ * emission site, and the one the D3 brief did not name. Same ruling,
1382
+ * same two-space indent; see stretchLine above for the argument. */
1293
1383
  export function turnFold(t, W) {
1294
1384
  const p = palette();
1295
1385
  // R3b (owner, 2026-08-27): ZERO TERMS ARE DROPPED. W14 always wrote
@@ -1353,13 +1443,13 @@ export function turnFold(t, W) {
1353
1443
  const keyW = KEY.length;
1354
1444
  const key = `${p.dim}${KEY}${p.reset}`;
1355
1445
  if (words === "") {
1356
- const keyed = `${p.bold}✦${p.reset} ${meta}${key}`;
1446
+ const keyed = ` ${meta}${key}`;
1357
1447
  if (visibleWidth(keyed) <= W)
1358
1448
  return [keyed];
1359
1449
  let tightMeta = meta;
1360
1450
  for (const [long, short] of COMPACT) {
1361
1451
  tightMeta = tightMeta.replaceAll(long, short);
1362
- const tight = `${p.bold}✦${p.reset} ${tightMeta}${key}`;
1452
+ const tight = ` ${tightMeta}${key}`;
1363
1453
  if (visibleWidth(tight) <= W)
1364
1454
  return [tight];
1365
1455
  }
@@ -1375,22 +1465,22 @@ export function turnFold(t, W) {
1375
1465
  // closed in the thinking fold, still open here. A width with no
1376
1466
  // room for the mark is a width with nothing to say: the row is
1377
1467
  // a hard cut of what it would have said.
1378
- const cut = `${p.bold}✦${p.reset} ${widthCut(tightMeta, Math.max(1, W - 3))}…`;
1379
- return [visibleWidth(cut) <= W ? cut : cutLine(`${p.bold}✦${p.reset} ${tightMeta}`, W)];
1468
+ const cut = ` ${widthCut(tightMeta, Math.max(1, W - 3))}…`;
1469
+ return [visibleWidth(cut) <= W ? cut : cutLine(` ${tightMeta}`, W)];
1380
1470
  }
1381
- return [`${p.bold}✦${p.reset} ${widthCut(tightMeta, room)}…${key}`];
1471
+ return [` ${widthCut(tightMeta, room)}…${key}`];
1382
1472
  }
1383
1473
  // A9 (ruling R2, mock A): the user chip rides the fold — the human's
1384
1474
  // words LEAD the one line, the same SGR-7 bracket as the live user
1385
1475
  // row (#16f, side pads included). The words take the fold's width
1386
- // budget: W − the gutter ("✦ " = 2) − the join (" · " = 3) − the
1476
+ // budget: W − the gutter (two spaces = 2, R6/D3) − the join (3) − the
1387
1477
  // chip's side pads (2) − the cut-tail reserve (1, the "…") − the
1388
1478
  // metadata's own width — the metadata survives, the words width-cut
1389
1479
  // at the end with the honest "…" (the "…" alone is the honest floor:
1390
1480
  // the words were there, cut).
1391
- const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6 - keyW);
1481
+ const budget = Math.max(0, W - visibleWidth(` ${meta}`) - 6 - keyW);
1392
1482
  const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
1393
- const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
1483
+ const row = ` ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
1394
1484
  if (visibleWidth(row) <= W)
1395
1485
  return [row];
1396
1486
  // R3h: the same COMPACT ladder the wordless branch walks — the nouns
@@ -1398,7 +1488,7 @@ export function turnFold(t, W) {
1398
1488
  let chipMeta = meta;
1399
1489
  for (const [long, short] of COMPACT) {
1400
1490
  chipMeta = chipMeta.replaceAll(long, short);
1401
- const tighter = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${chipMeta}${key}`;
1491
+ const tighter = ` ${p.rv} ${cut} ${p.rvEnd} · ${chipMeta}${key}`;
1402
1492
  if (visibleWidth(tighter) <= W)
1403
1493
  return [tighter];
1404
1494
  }
@@ -1412,10 +1502,10 @@ export function turnFold(t, W) {
1412
1502
  // bracket's pads, the join and the "…" are ten cells before a
1413
1503
  // single character of content, so every width below ten threw
1414
1504
  // invariant ① — see the wordless branch above for the same class.
1415
- const tail = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), Math.max(1, W - 8 - visibleWidth(cut)))}…`;
1416
- return [visibleWidth(tail) <= W ? tail : cutLine(`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd}`, W)];
1505
+ const tail = ` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), Math.max(1, W - 8 - visibleWidth(cut)))}…`;
1506
+ return [visibleWidth(tail) <= W ? tail : cutLine(` ${p.rv} ${cut} ${p.rvEnd}`, W)];
1417
1507
  }
1418
- return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), room)}…${key}`];
1508
+ return [` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), room)}…${key}`];
1419
1509
  }
1420
1510
  // ---- the bounded-block flow contract (W7, W8, W10) ----
1421
1511
  /** The caps — screen rows counted AFTER the fold, at the current width
@@ -1542,12 +1632,20 @@ function errorBody(c, W) {
1542
1632
  function liveWindow(text, W) {
1543
1633
  const p = palette();
1544
1634
  if (text === "") {
1545
- return [`${p.dim}${BODY_ROW}${p.reset}`, `${p.dim}${BODY_ROW}${p.reset}`, `${p.dim}${CUT_ROW}waiting for output${p.reset}`];
1635
+ // R7a: blank, not two bare gutters. A `│` marks a row that HAS
1636
+ // content; two of them above "waiting for output" drew a tall
1637
+ // empty bar under every command that had not printed yet — which
1638
+ // is most of them, for their first second.
1639
+ // ...and the waiting row sits DIRECTLY under its header, with the
1640
+ // blanks below it — VD-4's own rule ("the output starts under its
1641
+ // own header and grows downward"), which the gutter rows used to
1642
+ // satisfy by accident and blanks made visible as a two-row gap.
1643
+ return [`${p.dim}${CUT_ROW}waiting for output${p.reset}`, "", ""];
1546
1644
  }
1547
1645
  const rows = blockRows(text, W);
1548
1646
  if (rows.length <= CAP_LIVE_WINDOW) {
1549
1647
  while (rows.length < CAP_LIVE_WINDOW)
1550
- rows.push(`${p.dim}${BODY_ROW}${p.reset}`);
1648
+ rows.push(""); // R7a: blank, not a bar
1551
1649
  return rows;
1552
1650
  }
1553
1651
  const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - (CAP_LIVE_WINDOW - 1)} earlier rows · ctrl+r${p.reset}`, W);
@@ -1593,7 +1691,7 @@ function shellLiveTail(text, W) {
1593
1691
  return liveWindow("", W);
1594
1692
  const kept = rows.slice(Math.max(0, rows.length - (CAP_LIVE_WINDOW - 1)));
1595
1693
  while (kept.length < CAP_LIVE_WINDOW - 1)
1596
- kept.push(`${p.dim}${BODY_ROW}${p.reset}`);
1694
+ kept.push(""); // R7a: blank, not a bar
1597
1695
  return [...kept, cutLine(`${p.dim}${CUT_ROW}live tail · esc stop · alt+⏎ redirect${p.reset}`, W)];
1598
1696
  }
1599
1697
  /**
@@ -1642,10 +1740,10 @@ export function slotTail(text, W, rows) {
1642
1740
  const all = blockRows(text, W);
1643
1741
  const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(BODY_ROW));
1644
1742
  const body = from < 0 ? [] : all.slice(from);
1645
- const kept = body.slice(Math.max(0, body.length - rows));
1646
- while (kept.length < rows)
1647
- kept.push(`${p.dim}${BODY_ROW}${p.reset}`);
1648
- return kept;
1743
+ // R7a: no pad. The slot stopped padding (see slotPad) and this was
1744
+ // the same pad by another route — three blank rows under a call with
1745
+ // nothing to say yet, which is the hole a7's blank-run guard prices.
1746
+ return body.slice(Math.max(0, body.length - rows));
1649
1747
  }
1650
1748
  /** R4 — clamp or pad assembled slot rows to EXACTLY `rows`. The padding
1651
1749
  * is what makes the slot stand; the clamp is what keeps the slot from
@@ -1654,11 +1752,24 @@ export function slotTail(text, W, rows) {
1654
1752
  export function slotPad(content, rows) {
1655
1753
  if (rows <= 0)
1656
1754
  return [];
1657
- const p = palette();
1658
- const out = content.slice(0, rows);
1659
- while (out.length < rows)
1660
- out.push(`${p.dim}${BODY_ROW}${p.reset}`);
1661
- return out;
1755
+ // R7a THE SLOT NO LONGER PADS. It caps, and that is all.
1756
+ //
1757
+ // R4 padded to a fixed height because the slot's content came and
1758
+ // went: a finished call left the block, the block shrank, and every
1759
+ // row above it moved. The pad bought stability with rows drawn as
1760
+ // `│`, which is why a tall empty gutter ran down the screen under
1761
+ // every short block — the owner's own screenshot, and law 1.3's
1762
+ // case: a mark on a row with nothing to mark.
1763
+ //
1764
+ // Blanking the gutter revealed the hole it had been covering, and
1765
+ // the a7 replay priced the hole: blank runs over 2 in 653 of 733
1766
+ // frames, the screen never durably filling. So the pad had to go —
1767
+ // and the height it was buying is now bought by the CONTENT, since
1768
+ // R7a keeps every call's row for the life of the stretch. A block
1769
+ // whose rows only accumulate cannot shrink, so there is nothing
1770
+ // left for a pad to hold up. Measured: 65 of 733 at 40x24, the
1771
+ // pre-R7a number exactly, with the motion gates still green.
1772
+ return content.slice(0, rows);
1662
1773
  }
1663
1774
  /** R4 — the slot's overflow row: the calls in flight beyond the head
1664
1775
  * budget. It lives INSIDE the slot (it is one of the four rows), which
package/dist/render.d.ts CHANGED
@@ -77,6 +77,23 @@ export interface Palette {
77
77
  * spans and must end without stranding them. */
78
78
  readonly wash: string;
79
79
  readonly washEnd: string;
80
+ /** R7a — THE FOCUS MARKER'S EMPHASIS, and it is not a background.
81
+ *
82
+ * DC-3 gave the `ctrl+r` token the wash, which is a BACKGROUND once
83
+ * a ground is resolved: `48;5;236` on dark reads as a black block
84
+ * behind the key, on a row that is otherwise plain text. The owner
85
+ * asked for it gone. The invariant DC-3 was serving — exactly one
86
+ * bright token per frame, because the key has exactly one target —
87
+ * never required a background; it required CONTRAST against the dim
88
+ * siblings, and full-strength bold foreground is more of it than a
89
+ * wash was.
90
+ *
91
+ * Three escapes because "dim" has two forms here: SGR 2 in the
92
+ * neutral palette, a 256-colour foreground in the resolved ones.
93
+ * 22 cancels the attribute, 39 restores the default foreground, 1
94
+ * is the emphasis. It closes by re-opening the palette's own dim,
95
+ * like `washEnd`, so the surrounding span survives. */
96
+ readonly lift: string;
80
97
  readonly reset: string;
81
98
  }
82
99
  /**
package/dist/render.js CHANGED
@@ -28,7 +28,7 @@ const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32
28
28
  * 4's principle exactly: when the ground is unknown, use the thing that
29
29
  * is correct on any ground rather than guessing one.
30
30
  */
31
- const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim) => ({ ...BASE, red, dim, wash, washEnd, code: wash });
31
+ const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim) => ({ ...BASE, red, dim, wash, washEnd, code: wash, lift: "\x1b[22m\x1b[39m\x1b[1m" });
32
32
  /**
33
33
  * DC-3 — one table per ground.
34
34
  *
@@ -56,7 +56,7 @@ export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m", "\x1b[38;5;173m
56
56
  * not been established. Unchanged in every byte except `code`, which
57
57
  * was the defect. */
58
58
  export const COLOR_ON = COLOR_NEUTRAL;
59
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", reset: "" };
59
+ export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", lift: "", reset: "" };
60
60
  /** DC-3 — the resolved ground, set once at startup when the terminal
61
61
  * answers (see `ground.ts`). It starts UNKNOWN and may stay that way
62
62
  * forever; that is a supported state, not a failure. */
package/dist/strings.d.ts CHANGED
@@ -155,7 +155,19 @@ export declare function displayVerb(name: string): string;
155
155
  * The two ask-only gestures keep their clauses because the ask is the
156
156
  * only flavor with a set to build and an answer to write.
157
157
  */
158
- export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE or click confirms \u00B7 1-4 instant \u00B7 space toggles \u00B7 t types";
158
+ /**
159
+ * R6/D2 — the row claims only what is TRUE OF EVERY panel.
160
+ *
161
+ * Its old sentence ("⏎ or click confirms · 1-4 instant · space toggles")
162
+ * was documented as true of every flavor, and had silently stopped
163
+ * being: `1-4 instant` is false on the ask's multi-select and on the
164
+ * pick panel (where a digit only moves the cursor and never commits),
165
+ * `or click` is false on both (their frames deliberately report no
166
+ * clickable span), and "1-4" is wrong whenever a panel has a different
167
+ * option count. Layered honesty: this row states the invariant, and each
168
+ * panel's own affordance row states that panel's whole truth.
169
+ */
170
+ export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE confirms \u00B7 digits act on their row \u00B7 t types";
159
171
  /**
160
172
  * TUI2-R1 (D) — the sheet, one screen, static.
161
173
  *
package/dist/strings.js CHANGED
@@ -249,7 +249,19 @@ export function displayVerb(name) {
249
249
  * The two ask-only gestures keep their clauses because the ask is the
250
250
  * only flavor with a set to build and an answer to write.
251
251
  */
252
- export const PANEL_KEYS_ROW = "panels: ↑↓ move · ⏎ or click confirms · 1-4 instant · space toggles · t types";
252
+ /**
253
+ * R6/D2 — the row claims only what is TRUE OF EVERY panel.
254
+ *
255
+ * Its old sentence ("⏎ or click confirms · 1-4 instant · space toggles")
256
+ * was documented as true of every flavor, and had silently stopped
257
+ * being: `1-4 instant` is false on the ask's multi-select and on the
258
+ * pick panel (where a digit only moves the cursor and never commits),
259
+ * `or click` is false on both (their frames deliberately report no
260
+ * clickable span), and "1-4" is wrong whenever a panel has a different
261
+ * option count. Layered honesty: this row states the invariant, and each
262
+ * panel's own affordance row states that panel's whole truth.
263
+ */
264
+ export const PANEL_KEYS_ROW = "panels: ↑↓ move \u00b7 ⏎ confirms \u00b7 digits act on their row \u00b7 t types";
253
265
  /** The sheet's grid: the first six bindings in two 3-column rows, the
254
266
  * last four in two 2-column rows (the wide entries get the room). The
255
267
  * COLUMN STOPS are the prototype's absolute positions, floored by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",