@vincemakes/kiso-tui-cells 0.38.0 → 0.39.1

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.
@@ -306,8 +306,30 @@ export declare function diffBody(diff: import("./diff.js").DiffLine[] | null, W:
306
306
  export declare const CAP_TASK_LIVE = 6;
307
307
  /** W20 — the settled block's duration, the `2h 14m` form (the task
308
308
  * narrative's long-horizon idiom): minutes+seconds under an hour,
309
- * hours+minutes past it. */
309
+ * hours+minutes past it. Unchanged. */
310
310
  export declare function formatDuration(totalSeconds: number): string;
311
+ /** The LIVE elapsed label — every place a duration is shown while it is
312
+ * still running, and on the card that settles from it, so a card and the
313
+ * status row can never disagree.
314
+ *
315
+ * It replaced the hand-written second counts, which is how the status
316
+ * row came to read "working 637s": ten minutes as a four-figure number,
317
+ * with no branch anywhere that said otherwise. Past an hour it keeps
318
+ * seconds, because a clock the user is watching tick should not stop
319
+ * ticking. */
320
+ export declare function elapsedLabel(totalSeconds: number): string;
321
+ /** The SETTLED call's duration — R13's grammar (`exit 0 · 90 lines ·
322
+ * 0.4s`). A finished call knows its length to a tenth, which is
323
+ * information a running clock cannot have and a finished one should not
324
+ * throw away: under a minute the tenth stays.
325
+ *
326
+ * Past a minute the tenth stops being the interesting digit and the
327
+ * live label takes over — a twelve-minute call settled as `734.2s`, the
328
+ * same unreadable four-figure count the status row had. NOTHING under a
329
+ * minute moves, which is every duration the suite pinned before this.
330
+ * The suffix is part of the label (the live form's already is), so no
331
+ * caller appends its own `s`. */
332
+ export declare function settledLabel(totalSeconds: number): string;
311
333
  /** The status container's row: the status text (+ the tail) with the
312
334
  * right-aligned "/ commands · ↑ history" hint in the idle state —
313
335
  * the hint CUT FIRST when the width is short (the #16g rule); when
@@ -644,7 +644,7 @@ class ToolExecution {
644
644
  out.push(...toolBlockBody(c, W, ctx));
645
645
  return out;
646
646
  }
647
- const elapsed = c.startedAt !== null && c.doneAt !== null ? ((c.doneAt - c.startedAt) / 1000).toFixed(1) : "?";
647
+ const elapsed = c.startedAt !== null && c.doneAt !== null ? settledLabel((c.doneAt - c.startedAt) / 1000) : "?s";
648
648
  // TUI2-R1.5 ⑤ (VD-6): the line count is stated EXACTLY ONCE. Every
649
649
  // read card carried it twice — `(2 lines, 0.0s) · 2 lines · ctrl+o
650
650
  // expands` — because the parens and the suffix were written by
@@ -726,7 +726,7 @@ class ToolExecution {
726
726
  // which let a narrow row cut the affordance off entirely.
727
727
  // The tier ladder is what gives way; the key never is.
728
728
  const keySuffix = c.expanded ? ` · ${COLLAPSE_ROW}` : "";
729
- const words = pickTier([join(meta, counted, `${elapsed}s`, attr), join(meta, counted, `${elapsed}s`), join(meta, `${elapsed}s`), meta], W - visibleWidth(noteIndent()) - keySuffix.length);
729
+ const words = pickTier([join(meta, counted, elapsed, attr), join(meta, counted, elapsed), join(meta, elapsed), meta], W - visibleWidth(noteIndent()) - keySuffix.length);
730
730
  // DC-50 / R14 — ONE CARD, ONE SKELETON, expanded or not.
731
731
  //
732
732
  // The expanded card used to take a DIFFERENT shape: its head
@@ -780,7 +780,7 @@ class ToolExecution {
780
780
  // ("compil 1s"). The head is cut against the room the duration
781
781
  // leaves; the duration then rides the row, always legible.
782
782
  const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
783
- const dur = ` · ${elapsed}s`;
783
+ const dur = ` · ${elapsedLabel(elapsed)}`;
784
784
  // R3 (design §5.2): a running command BREATHES — one glyph, seven
785
785
  // greys, bottoming out on the ground's dim token (§2.2 applies
786
786
  // mid-animation, not just at rest). The quadrant spinner it
@@ -823,7 +823,7 @@ class ToolExecution {
823
823
  // rewrites it in place: `3s · esc stops` → `exit 0 · 90 lines ·
824
824
  // 3.2s`.
825
825
  const gestures = c.name === "shell" ? " · esc stops · alt+⏎ redirects" : "";
826
- const status = pickTier([`${elapsed}s${gestures}`, `${elapsed}s`], Math.max(1, W - visibleWidth(noteIndent())));
826
+ const status = pickTier([`${elapsedLabel(elapsed)}${gestures}`, elapsedLabel(elapsed)], Math.max(1, W - visibleWidth(noteIndent())));
827
827
  const live = toolBlockBody(c, W, ctx);
828
828
  if (live.length > 0)
829
829
  return slabBlock(gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, W)[0], live, status, W);
@@ -846,7 +846,7 @@ class ToolExecution {
846
846
  // way so the COMMAND keeps something to say, and the elapsed —
847
847
  // pin 4's core — never does.
848
848
  const MIN_TARGET = 10; // the gutter, the verb column, a character of command
849
- const oneRow = pickTier([`${elapsed}s${gestures}`, `${elapsed}s`], Math.max(1, W - MIN_TARGET - 3));
849
+ const oneRow = pickTier([`${elapsedLabel(elapsed)}${gestures}`, elapsedLabel(elapsed)], Math.max(1, W - MIN_TARGET - 3));
850
850
  const room = Math.max(4, W - visibleWidth(oneRow) - 3);
851
851
  const only = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, room)[0];
852
852
  return slabBlock(`${only}${p.dim} · ${oneRow}${p.reset}`, [], null, W);
@@ -854,7 +854,10 @@ class ToolExecution {
854
854
  // W2: ◦ replaces → for QUEUED — · is the separator inside every
855
855
  // metadata group; a queued marker that is also the separator
856
856
  // glyph reads as noise
857
- return gutterCut(`${p.dim}◦${p.reset} `, `${verbCol} ${liveTarget(c)}`, W);
857
+ // The `◦` carried "not started yet" ALONE, and a marker is not a
858
+ // word: on a screen where running and queued rows sit together, a
859
+ // reader has to already know the glyph. The suffix says it.
860
+ return gutterCut(`${p.dim}◦${p.reset} `, `${verbCol} ${liveTarget(c)}${p.dim} · queued${p.reset}`, W);
858
861
  }
859
862
  }
860
863
  // ---- TUI2-R1 (A): the self-naming expand affordance ----
@@ -951,18 +954,18 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "
951
954
  // first, then the count, and the core — what happened and how long it
952
955
  // took — is never cut open; below that the target itself truncates.
953
956
  const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
954
- const core = join(meta, counted, `${elapsed}s`);
955
- const withAttr = join(meta, counted, `${elapsed}s`, attr.replace(" · ", ""));
957
+ const core = join(meta, counted, elapsed);
958
+ const withAttr = join(meta, counted, elapsed, attr.replace(" · ", ""));
956
959
  const lead = `${verbCol} `;
957
960
  const row = (tail) => `${lead}${target}${tail === "" ? "" : ` · ${tail}`}`;
958
961
  // 1. everything; 2. the attribution gives way; 2b. the COUNT gives
959
962
  // way next (pin 4), where the suffix is not already carrying it —
960
963
  // the target whole through all three
961
- const whole = firstFit([row(withAttr), row(core), row(join(meta, `${elapsed}s`))], room);
964
+ const whole = firstFit([row(withAttr), row(core), row(join(meta, elapsed))], room);
962
965
  if (whole !== null)
963
966
  return whole;
964
967
  // 3. the target truncates, the core stays whole
965
- const stem = join(meta, `${elapsed}s`);
968
+ const stem = join(meta, elapsed);
966
969
  const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
967
970
  if (budget >= 1)
968
971
  return `${lead}${widthCut(target, budget)}… · ${stem}`;
@@ -972,7 +975,7 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "
972
975
  // no bracket to leave open, so the reason retired with the
973
976
  // parentheses and pin 4's own rule applies at every width: what
974
977
  // happened and how long it took is never cut away.
975
- const floor = `${elapsed}s`;
978
+ const floor = elapsed;
976
979
  const left = room - visibleWidth(lead) - visibleWidth(floor) - 4; // the ellipsis + " · "
977
980
  if (left >= 1)
978
981
  return `${lead}${widthCut(target, left)}… · ${floor}`;
@@ -1780,15 +1783,57 @@ class Banner {
1780
1783
  * done-collapse. Every live row CUTS at W (never folds) — the block's
1781
1784
  * height is its row count. */
1782
1785
  export const CAP_TASK_LIVE = 6;
1783
- /** W20 — the settled block's duration, the `2h 14m` form (the task
1784
- * narrative's long-horizon idiom): minutes+seconds under an hour,
1785
- * hours+minutes past it. */
1786
- export function formatDuration(totalSeconds) {
1786
+ /** The two duration idioms, from ONE implementation.
1787
+ *
1788
+ * They agree in every branch but the hour: W20's settled task block says
1789
+ * `2h 14m` (a long-horizon narrative does not care about seconds), and a
1790
+ * LIVE elapsed label says `1h 2m 3s` (a running clock does). A near-copy
1791
+ * differing in one branch is the drift that a shared helper exists to
1792
+ * prevent, so the branch is a parameter.
1793
+ *
1794
+ * Negative is clamped: a clock skew is not a negative duration. */
1795
+ function duration(totalSeconds, hoursKeepSeconds) {
1787
1796
  const s = Math.max(0, Math.round(totalSeconds));
1788
1797
  if (s < 60)
1789
1798
  return `${s}s`;
1790
1799
  const m = Math.floor(s / 60);
1791
- return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
1800
+ if (m < 60)
1801
+ return `${m}m ${s % 60}s`;
1802
+ const h = Math.floor(m / 60);
1803
+ return hoursKeepSeconds ? `${h}h ${m % 60}m ${s % 60}s` : `${h}h ${m % 60}m`;
1804
+ }
1805
+ /** W20 — the settled block's duration, the `2h 14m` form (the task
1806
+ * narrative's long-horizon idiom): minutes+seconds under an hour,
1807
+ * hours+minutes past it. Unchanged. */
1808
+ export function formatDuration(totalSeconds) {
1809
+ return duration(totalSeconds, false);
1810
+ }
1811
+ /** The LIVE elapsed label — every place a duration is shown while it is
1812
+ * still running, and on the card that settles from it, so a card and the
1813
+ * status row can never disagree.
1814
+ *
1815
+ * It replaced the hand-written second counts, which is how the status
1816
+ * row came to read "working 637s": ten minutes as a four-figure number,
1817
+ * with no branch anywhere that said otherwise. Past an hour it keeps
1818
+ * seconds, because a clock the user is watching tick should not stop
1819
+ * ticking. */
1820
+ export function elapsedLabel(totalSeconds) {
1821
+ return duration(totalSeconds, true);
1822
+ }
1823
+ /** The SETTLED call's duration — R13's grammar (`exit 0 · 90 lines ·
1824
+ * 0.4s`). A finished call knows its length to a tenth, which is
1825
+ * information a running clock cannot have and a finished one should not
1826
+ * throw away: under a minute the tenth stays.
1827
+ *
1828
+ * Past a minute the tenth stops being the interesting digit and the
1829
+ * live label takes over — a twelve-minute call settled as `734.2s`, the
1830
+ * same unreadable four-figure count the status row had. NOTHING under a
1831
+ * minute moves, which is every duration the suite pinned before this.
1832
+ * The suffix is part of the label (the live form's already is), so no
1833
+ * caller appends its own `s`. */
1834
+ export function settledLabel(totalSeconds) {
1835
+ const tenths = Math.max(0, Math.round(totalSeconds * 10) / 10);
1836
+ return tenths < 60 ? `${tenths.toFixed(1)}s` : elapsedLabel(tenths);
1792
1837
  }
1793
1838
  /**
1794
1839
  * W20 — the task checklist as STATE: ONE live block that redraws in
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, 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, elapsedLabel, settledLabel, 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, 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, elapsedLabel, settledLabel, 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/md.js CHANGED
@@ -713,35 +713,36 @@ function tableRows(b, W) {
713
713
  if (cols === null)
714
714
  return recordRows(t, W);
715
715
  const p = palette();
716
- // R2: no rails. The drawn width is two columns of inset plus the
717
- // columns and their two-space gutters — a table is bounded by the
718
- // blank lines above and below it, exactly as every other block on the
719
- // screen is, and it was the last box left on a screen that has decided
720
- // not to have boxes. Alignment does the work the rails were doing, and
721
- // a copied table is closer to markdown without them.
716
+ // DECLARED REVERSAL (2026-09-17), TABLES ONLY. R2 removed the rails at
717
+ // the nineteen-screen review and MD-1.3 put ONE rule under the header;
718
+ // both are superseded here. Every other R2 hairline rule stands, and
719
+ // the record form keeps NO borders — it is the fallback, not a table.
720
+ //
721
+ // The rails are hairlines in the R2 hairline colour, and the styling
722
+ // goes on AFTER the measure, exactly as it does for a cell: a colour
723
+ // can never move a column.
724
+ const rule = (l, m, r) => `${p.dim}${l}${cols.map((w) => "\u2500".repeat(w + 2)).join(m)}${r}${p.reset}`;
725
+ const V = `${p.dim}\u2502${p.reset}`;
722
726
  const row = (cells, bold) => {
723
727
  const boxes = cells.map((c, i) => cellBox(c, cols[i], t.align[i], bold));
724
728
  const rows = [];
725
729
  for (let k = 0; k < Math.max(...boxes.map((x) => x.length)); k += 1) {
726
730
  // a short box pays its blanks so the columns to its right do not
727
731
  // move: a cell is a BOX, and the row is as tall as its tallest.
728
- rows.push(` ${boxes.map((x, i) => x[k] ?? " ".repeat(cols[i])).join(" ")}`.replace(/\s+$/, ""));
732
+ // Trailing space is NOT stripped any more — it sits inside the
733
+ // closing rail, and stripping it would pull the rail left.
734
+ rows.push(`${V}${boxes.map((x, i) => ` ${x[k] ?? " ".repeat(cols[i])} `).join(V)}${V}`);
729
735
  }
730
736
  return rows;
731
737
  };
732
- // MD-1.3 / R2 AMENDMENT 1 (owner ruling, 2026-09-11) — ONE rule under
733
- // the header row, at the grid's own width. R2 removed the RAILS: the
734
- // four-sided box that BOUNDS a table. This bounds nothing; it SEPARATES
735
- // the header from the body, which is the one job the round's governing
736
- // distinction gives a rule — a rule separates, a gutter scopes, a rail
737
- // bounds. Rails stay out.
738
- //
739
- // What it buys is not decoration: without it a six-row table's header
740
- // was carried by SGR bold ALONE, so in a pipe, under NO_COLOR, or on a
741
- // terminal with weak bold, seven identical rows arrived with nothing
742
- // saying which one names the columns.
743
- const ruleW = cols.reduce((n, w) => n + w, 0) + Math.max(0, cols.length - 1) * 2;
744
- return [...row(t.header, true), ` ${p.dim}${"\u2500".repeat(ruleW)}${p.reset}`, ...t.rows.flatMap((r) => row(r, false))];
738
+ const body = t.rows.flatMap((r, i) => (i === 0 ? row(r, false) : [rule("\u251c", "\u253c", "\u2524"), ...row(r, false)]));
739
+ return [
740
+ rule("\u250c", "\u252c", "\u2510"),
741
+ ...row(t.header, true),
742
+ rule("\u251c", "\u253c", "\u2524"),
743
+ ...body,
744
+ rule("\u2514", "\u2534", "\u2518"),
745
+ ];
745
746
  }
746
747
  /** A cell's column count: what a human sees, styling removed. */
747
748
  function cellWidth(cell) {
@@ -753,13 +754,22 @@ function cellWidth(cell) {
753
754
  * report's sample stopped reading as a table — and it is the one number
754
755
  * that decides when the record form is still the better answer. */
755
756
  const CELL_FLOOR = 8;
756
- /** The drawn width of a grid with these columns, by the measure the R2
757
- * table has always used: the two-column inset plus every column AND its
758
- * two-space gutter. Conservative by one gutter (the last column has
759
- * none), which is where the table's right margin comes from — kept as
760
- * it was, because the record threshold has always been stated in it. */
757
+ /** The drawn width of a grid with these columns.
758
+ *
759
+ * DECLARED REVERSAL (2026-09-17, tables only): the R2 measure was the
760
+ * two-column inset plus every column and its two-space gutter — the
761
+ * shape of a table with NO RAILS. With rails it is `sum + 3n + 1`: one
762
+ * rail between every pair of columns and one at each edge (n+1), and one
763
+ * space of padding inside every rail (2n).
764
+ *
765
+ * This function's old comment said the record threshold "has always been
766
+ * stated in it", and that is exactly why changing it IS the re-cut: the
767
+ * grid costs n−1 columns more than the railless form — 2 for a
768
+ * three-column table, 6 for a seven-column one — and the record fallback
769
+ * arrives that much earlier. Nothing else moves: the greedy shrink, the
770
+ * CELL_FLOOR of 8 and the fallback criterion are untouched. */
761
771
  function gridWidth(cols) {
762
- return cols.reduce((n, w) => n + w + 2, 2);
772
+ return cols.reduce((n, w) => n + w, 0) + 3 * cols.length + 1;
763
773
  }
764
774
  /**
765
775
  * MD-1.1 — take one column off the WIDEST column until the grid fits.
package/dist/render.js CHANGED
@@ -220,8 +220,14 @@ function toolSummaryDetail(name, input, result) {
220
220
  const exit = exitCodeOf(result);
221
221
  return `${command} (exit ${exit})`;
222
222
  }
223
- case "list_dir":
224
- return String(input.path ?? "(root)");
223
+ case "list_dir": {
224
+ // "(root)" already answered an ABSENT path. A model that sends
225
+ // "." explicitly is making the same request and was getting the
226
+ // dot on the row. `./` likewise. Anything else — including `..`
227
+ // and a dotfile directory like `.github` — is a real path.
228
+ const dir = String(input.path ?? ".");
229
+ return dir === "." || dir === "./" ? "(root)" : dir;
230
+ }
225
231
  case "search_text":
226
232
  return searchSubject(input);
227
233
  default:
@@ -261,8 +267,14 @@ function toolTargetRaw(name, input) {
261
267
  return String(input.path ?? "?");
262
268
  case "shell":
263
269
  return String(input.command ?? "?");
264
- case "list_dir":
265
- return String(input.path ?? "(root)");
270
+ case "list_dir": {
271
+ // "(root)" already answered an ABSENT path. A model that sends
272
+ // "." explicitly is making the same request and was getting the
273
+ // dot on the row. `./` likewise. Anything else — including `..`
274
+ // and a dotfile directory like `.github` — is a real path.
275
+ const dir = String(input.path ?? ".");
276
+ return dir === "." || dir === "./" ? "(root)" : dir;
277
+ }
266
278
  case "search_text":
267
279
  return searchSubject(input);
268
280
  default:
package/dist/strings.d.ts CHANGED
@@ -188,15 +188,7 @@ export declare function keysSheetRows(W: number): string[];
188
188
  * sheet renders, joined — so the two can disagree only by deleting a
189
189
  * test. The sheet is the readable form; this is the greppable one. */
190
190
  export declare function keysHelpRow(): string;
191
- /**
192
- * KC3.5 slice ⓪ (the extraction) — the /help command table.
193
- *
194
- * The rows were eight bodyLog calls in the CLI's dispatcher; they are
195
- * presentation, and presentation belongs here (the KC3 §1 pattern —
196
- * the FLOW, which is "print these on the chain, then re-prompt", stays
197
- * in dispatch.ts). The last row carries its own newline exactly as it
198
- * did inline: bodyLog splits on \n, so `exit` and `keys` land as two
199
- * rows from one call — the shape the KC1/KC2/KC3 gestures were added
200
- * to, unchanged.
201
- */
191
+ /** The slash commands `/help` lists. The keys and the `!` gestures in the
192
+ * table are not commands and are not here. */
193
+ export declare function slashCommandNames(): readonly string[];
202
194
  export declare function helpRows(): string[];
package/dist/strings.js CHANGED
@@ -430,51 +430,65 @@ export function keysHelpRow() {
430
430
  * rows from one call — the shape the KC1/KC2/KC3 gestures were added
431
431
  * to, unchanged.
432
432
  */
433
+ /** The rows `/help` prints, as DATA — the one place that says which
434
+ * commands exist. `helpRows` renders it; `slashCommandNames` asks it
435
+ * whether a word is a command at all, so the dispatcher's error line
436
+ * cannot drift from the list the same screen prints. */
437
+ const HELP_TABLE = [
438
+ ["/help", "print this list of commands"],
439
+ ["/think", "show the last full thinking block"],
440
+ ["/last", "show the most recent tool call's input and output"],
441
+ // R4 (C4d): a committed row is the terminal's, and cannot be
442
+ // re-wrapped in place (ADR-0046) — this appends it re-folded.
443
+ ["/rewrap", "re-print the recent prose at the current width"],
444
+ ["/copy", "copy the last answer (raw markdown) — ctrl+x does the same"],
445
+ ["/status", "show session id, event count, and context estimate"],
446
+ // A command with no row is a command nobody can find. `/context` has
447
+ // been dispatchable since TUI2-R1 slice 6 and was never listed here,
448
+ // so the only way to learn it existed was to read the source.
449
+ ["/context", "show where the context went — the per-request rent ledger"],
450
+ ["/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"],
451
+ ["/model", "list model profiles; /model <name|provider/model> switches"],
452
+ ["/compact", "summarize the older conversation to free context"],
453
+ ["/clear", "start a fresh conversation (the old session stays resumable)"],
454
+ ["/resume", "switch to another session; /resume <id> goes directly"],
455
+ // §2.5: the conversation is untouched — this rereads what kiso was
456
+ // built with, not what it has said.
457
+ ["/reload", "reread extensions, skills and config into this session"],
458
+ // §2.2: the two shell gestures and their one escape. They sit
459
+ // beside the slash commands because that is what a reader is
460
+ // looking for when they look here, even though `!` is not one.
461
+ // §2.3: the switch belongs beside ctrl+o's job, and a gesture the
462
+ // sheet does not name is a gesture nobody uses (DC-30, DC-36).
463
+ ["ctrl+t", "fold the thinking blocks, and fold them back"],
464
+ // §2.4: the composer, in your own editor. It names the variables
465
+ // because that is what a reader has to set for it to work.
466
+ ["ctrl+g", "edit the composer in $VISUAL or $EDITOR — the text comes back unsent"],
467
+ ["!<cmd>", "run a shell command and send it with its output as your turn"],
468
+ ["!!<cmd>", "run one and show it here only — the model never sees it"],
469
+ ["\\!", "send a line that really starts with ! (the only escape)"],
470
+ ["exit", "leave the session"],
471
+ // TUI2-R1 (D): the SENTENCE is deliberately unchanged. Deriving it
472
+ // from KEY_BINDINGS would be an improvement and it would also move
473
+ // an assertion outside the round's declared supersession classes,
474
+ // so the sheet stays the derived surface and this row keeps its
475
+ // words. `keysHelpRow()` exists for the round allowed to swap it;
476
+ // until then the drift guard is the test that every binding in the
477
+ // table is mentioned here. DC-1 changes the PADDING, not the words.
478
+ ["keys", "enter sends \u00b7 ctrl+J newline (shift+enter where encoded) \u00b7 esc stops the run \u00b7 alt+\u23ce stops it and sends this instead \u00b7 @ files \u00b7 1-4 answers an ask"],
479
+ ];
480
+ /** The slash commands `/help` lists. The keys and the `!` gestures in the
481
+ * table are not commands and are not here. */
482
+ export function slashCommandNames() {
483
+ return HELP_TABLE.map(([name]) => name).filter((n) => n.startsWith("/"));
484
+ }
433
485
  export function helpRows() {
434
486
  const p = palette();
435
487
  // DC-1: ONE description column. The gap used to be four spaces after
436
488
  // the name whatever the name's length, so `/help`'s description began
437
489
  // three columns left of `/compact`'s and the second column wandered
438
490
  // down the list. displayWidth is the authority, as everywhere else.
439
- const table = [
440
- ["/help", "print this list of commands"],
441
- ["/think", "show the last full thinking block"],
442
- ["/last", "show the most recent tool call's input and output"],
443
- // R4 (C4d): a committed row is the terminal's, and cannot be
444
- // re-wrapped in place (ADR-0046) — this appends it re-folded.
445
- ["/rewrap", "re-print the recent prose at the current width"],
446
- ["/copy", "copy the last answer (raw markdown) — ctrl+x does the same"],
447
- ["/status", "show session id, event count, and context estimate"],
448
- ["/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"],
449
- ["/model", "list model profiles; /model <name|provider/model> switches"],
450
- ["/compact", "summarize the older conversation to free context"],
451
- ["/clear", "start a fresh conversation (the old session stays resumable)"],
452
- ["/resume", "switch to another session; /resume <id> goes directly"],
453
- // §2.5: the conversation is untouched — this rereads what kiso was
454
- // built with, not what it has said.
455
- ["/reload", "reread extensions, skills and config into this session"],
456
- // §2.2: the two shell gestures and their one escape. They sit
457
- // beside the slash commands because that is what a reader is
458
- // looking for when they look here, even though `!` is not one.
459
- // §2.3: the switch belongs beside ctrl+o's job, and a gesture the
460
- // sheet does not name is a gesture nobody uses (DC-30, DC-36).
461
- ["ctrl+t", "fold the thinking blocks, and fold them back"],
462
- // §2.4: the composer, in your own editor. It names the variables
463
- // because that is what a reader has to set for it to work.
464
- ["ctrl+g", "edit the composer in $VISUAL or $EDITOR — the text comes back unsent"],
465
- ["!<cmd>", "run a shell command and send it with its output as your turn"],
466
- ["!!<cmd>", "run one and show it here only — the model never sees it"],
467
- ["\\!", "send a line that really starts with ! (the only escape)"],
468
- ["exit", "leave the session"],
469
- // TUI2-R1 (D): the SENTENCE is deliberately unchanged. Deriving it
470
- // from KEY_BINDINGS would be an improvement and it would also move
471
- // an assertion outside the round's declared supersession classes,
472
- // so the sheet stays the derived surface and this row keeps its
473
- // words. `keysHelpRow()` exists for the round allowed to swap it;
474
- // until then the drift guard is the test that every binding in the
475
- // table is mentioned here. DC-1 changes the PADDING, not the words.
476
- ["keys", "enter sends \u00b7 ctrl+J newline (shift+enter where encoded) \u00b7 esc stops the run \u00b7 alt+\u23ce stops it and sends this instead \u00b7 @ files \u00b7 1-4 answers an ask"],
477
- ];
491
+ const table = HELP_TABLE;
478
492
  const stop = Math.max(...table.map(([name]) => displayWidth(name))) + 4;
479
493
  const cmd = (name, desc) => `${p.bold}${name}${p.reset}${" ".repeat(stop - displayWidth(name))}${desc}`;
480
494
  const rows = table.slice(0, -2).map(([name, desc]) => cmd(name, desc));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.38.0",
3
+ "version": "0.39.1",
4
4
  "description": "kiso tui-cells \u2014 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",