@vincemakes/kiso-tui-cells 0.16.6 → 0.16.7

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.
@@ -280,30 +280,6 @@ export declare function exploreRows(parts: readonly {
280
280
  name: string;
281
281
  subjects: readonly string[];
282
282
  }[], W: number): string[];
283
- /** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
284
- * scrollback, becomes ONE line — the work order's claimed shape
285
- * (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
286
- * toolStart: read_file → "reads", edit_file → "edits", the other tools
287
- * as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
288
- * in, the verb + "s" otherwise).
289
- * A9 (ruling R2, mock A): the user chip rides the fold — the human's
290
- * words LEAD the one line, `▞ <chip> · thought 19s · 5 reads · no
291
- * edits` — the chip the SAME SGR-7 bracket as the live user row (#16f,
292
- * side pads included). The words take the fold's width budget: the
293
- * metadata terms survive (the W14 metadata rule — they give way LAST),
294
- * the words width-cut at the end with the honest "…" (never a silent
295
- * truncate — invariant ① holds on the ONE row by construction). */
296
- /**
297
- * R3b — what a run of work DID, in words. One definition, because two
298
- * surfaces say it: the fold line (`turnFold`, above) and the expand
299
- * header the compositor writes when that fold is opened. A second copy
300
- * would be a second answer to the same question, and the first thing to
301
- * drift would be the plurals — `search_text` is "matches", not
302
- * "searchs", and only the ROLLUP_NOUN table knows that.
303
- *
304
- * Zero terms are dropped (owner ruling, R3b): a term earns its place by
305
- * having a count.
306
- */
307
283
  export declare function foldTerms(reads: number, edits: number, others: readonly [string, number][]): string[];
308
284
  export declare function turnFold(t: {
309
285
  words: string;
@@ -289,7 +289,18 @@ class ThinkingFold {
289
289
  render(W, _ctx) {
290
290
  const p = palette();
291
291
  const block = this.cell.text;
292
- const trimmed = escapeTerminal(block.trim());
292
+ // R3f the fold is ONE ROW, so its text is one line.
293
+ //
294
+ // `escapeTerminal` strips C0 but KEEPS \n and \t, and
295
+ // `charWidth(0x0A)` is 1 — so a multi-line thinking block sailed
296
+ // through every width check as a legal single row, and the
297
+ // terminal then wrapped it across the chrome. That shipped in
298
+ // 0.16.6 and is what smashed the composer: a model whose thinking
299
+ // opens with a numbered plan ("1. …\n2. …") produces exactly it.
300
+ // Invariant ①b now catches the class at the emit; this stops
301
+ // producing it. Whitespace collapses because the row is a
302
+ // SUMMARY — the full text is one ctrl+r away, unchanged.
303
+ const trimmed = escapeTerminal(block.trim()).replace(/\s+/g, " ");
293
304
  // R2 (owner, 2026-08-27) — three changes, each independent.
294
305
  //
295
306
  // ITALIC marks the row as not-the-answer without spending a colour,
@@ -971,22 +982,46 @@ function countTerm(n, singular, plural) {
971
982
  * Zero terms are dropped (owner ruling, R3b): a term earns its place by
972
983
  * having a count.
973
984
  */
985
+ /**
986
+ * R3g (2026-08-28) — the fold's own terms, VERB + COUNT + NOUN.
987
+ *
988
+ * DECLARED SUPERSESSION. R3b built these terms out of ROLLUP_NOUN,
989
+ * whose own comment says not to: that table names what a single-tool
990
+ * rollup COUNTS ("5 matches" — five matched lines), and this line
991
+ * counts CALLS. So one search_text call rendered `1 match`, a sentence
992
+ * that is false whenever the search matched any other number — which is
993
+ * almost always. `shell` fell through to the verb branch and read
994
+ * `4 shells`.
995
+ *
996
+ * The phrasing is the owner's, from the shape they asked for:
997
+ * "thought 17s · read 4 files · listed 1 directory · ran 4 shell
998
+ * commands". A tool with no entry says `3 × <verb>`, which counts calls
999
+ * without inventing a noun for them.
1000
+ */
1001
+ const FOLD_TERM = {
1002
+ read_file: ["read", "file", "files"],
1003
+ edit_file: ["edited", "file", "files"],
1004
+ write_file: ["wrote", "file", "files"],
1005
+ list_dir: ["listed", "directory", "directories"],
1006
+ search_text: ["ran", "search", "searches"],
1007
+ shell: ["ran", "shell command", "shell commands"],
1008
+ };
1009
+ function foldTerm(name, n) {
1010
+ const t = FOLD_TERM[name];
1011
+ if (t === undefined)
1012
+ return `${n} × ${displayVerb(name)}`;
1013
+ return `${t[0]} ${n} ${n === 1 ? t[1] : t[2]}`;
1014
+ }
974
1015
  export function foldTerms(reads, edits, others) {
975
1016
  const parts = [];
976
1017
  if (reads > 0)
977
- parts.push(countTerm(reads, "read", "reads"));
1018
+ parts.push(foldTerm("read_file", reads));
978
1019
  if (edits > 0)
979
- parts.push(countTerm(edits, "edit", "edits"));
1020
+ parts.push(foldTerm("edit_file", edits));
980
1021
  for (const [name, n] of others) {
981
1022
  if (n === 0)
982
1023
  continue;
983
- const noun = ROLLUP_NOUN[name];
984
- if (noun !== undefined)
985
- parts.push(countTerm(n, noun.endsWith("es") ? noun.slice(0, -2) : noun.slice(0, -1), noun));
986
- else {
987
- const verb = displayVerb(name);
988
- parts.push(countTerm(n, verb, `${verb}s`));
989
- }
1024
+ parts.push(foldTerm(name, n));
990
1025
  }
991
1026
  return parts;
992
1027
  }
@@ -998,19 +1033,42 @@ export function turnFold(t, W) {
998
1033
  // edits, half the row was the half that said nothing. A term earns its
999
1034
  // place by having a count.
1000
1035
  const meta = [`thought ${t.thoughtSeconds}s`, ...foldTerms(t.reads, t.edits, t.others)].join(" · ");
1001
- const words = escapeTerminal(t.words);
1036
+ // R3f: the same one-row rule. This one is worse than the thinking
1037
+ // fold's — the fold is a COMMITTED row, so a multi-line user message
1038
+ // would desync the committed-line accounting permanently rather than
1039
+ // for one frame. (The live UserMessage chip keeps its multi-row form:
1040
+ // it folds per paragraph and each row is already one row.)
1041
+ const words = escapeTerminal(t.words).replace(/\s+/g, " ");
1042
+ // R3g (2026-08-28) — THE KEY IS LOAD-BEARING, SO IT GIVES WAY LAST.
1043
+ //
1044
+ // R3b wrote "the suffix gives way first at a narrow width", by
1045
+ // analogy with the settled card's affordance. The analogy does not
1046
+ // hold: the card's rows are still on the screen when its hint is
1047
+ // dropped, and a fold's are not. A fold with no key is the turn's
1048
+ // work behind a line with no way back — the one thing the fold's own
1049
+ // header says it must never be. So the META gives way first (a cut
1050
+ // count is still a true count), and the key survives.
1051
+ //
1052
+ // Two live cases, both reachable at W=80: the wordless fold went
1053
+ // keyless as soon as the terms grew (R3g's verb+noun phrasing pushed
1054
+ // it over), and the CHIP fold never had a key at all — it was
1055
+ // written without one, so every folded turn that carried the user's
1056
+ // words was unreachable by the key its siblings advertise. Found by
1057
+ // an independent review (fable) and then by the paced-rollup gate,
1058
+ // which got `kind: "none"` back from expandNext.
1059
+ const KEY = " · ctrl+r";
1060
+ const keyW = KEY.length;
1061
+ const key = `${p.dim}${KEY}${p.reset}`;
1002
1062
  if (words === "") {
1003
- const row = `${p.bold}✦${p.reset} ${meta}`;
1004
- // R3b: the fold NAMES ITS OWN KEY. Without it the segment's work is
1005
- // unreachable — no rollup row, no expand, nothing — and hiding a
1006
- // tool call behind a line with no way back is the one thing this
1007
- // product must not do. The suffix gives way first at a narrow
1008
- // width, on the same principle the settled card's does: an
1009
- // affordance that does not fit is dropped, never half-drawn.
1010
- const keyed = `${row}${p.dim} · ctrl+r${p.reset}`;
1063
+ const keyed = `${p.bold}✦${p.reset} ${meta}${key}`;
1011
1064
  if (visibleWidth(keyed) <= W)
1012
1065
  return [keyed];
1013
- return visibleWidth(row) <= W ? [row] : [`${p.bold}✦${p.reset} ${widthCut(meta, Math.max(1, W - 3))}…`]; // a wordless turn folds to the W14 shape
1066
+ // the meta cuts with the honest "…"; below the width where even
1067
+ // the key fits there is nothing to preserve and the row is a cut.
1068
+ const room = W - 2 - keyW - 1;
1069
+ if (room < 2)
1070
+ return [`${p.bold}✦${p.reset} ${widthCut(meta, Math.max(1, W - 3))}…`];
1071
+ return [`${p.bold}✦${p.reset} ${widthCut(meta, room)}…${key}`];
1014
1072
  }
1015
1073
  // A9 (ruling R2, mock A): the user chip rides the fold — the human's
1016
1074
  // words LEAD the one line, the same SGR-7 bracket as the live user
@@ -1020,15 +1078,19 @@ export function turnFold(t, W) {
1020
1078
  // metadata's own width — the metadata survives, the words width-cut
1021
1079
  // at the end with the honest "…" (the "…" alone is the honest floor:
1022
1080
  // the words were there, cut).
1023
- const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6);
1081
+ const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6 - keyW);
1024
1082
  const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
1025
- const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}`;
1083
+ const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
1026
1084
  if (visibleWidth(row) <= W)
1027
1085
  return [row];
1028
1086
  // the last resort: the METADATA gives way — the words hold their
1029
- // budget, the meta cuts with the honest "…"; invariant ① never trips
1030
- // at ANY width (a degenerate W's fold is a cut, never a crash).
1031
- return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(meta, Math.max(1, W - 8 - visibleWidth(cut)))}…`];
1087
+ // budget, the key holds its nine cells, the meta cuts with the honest
1088
+ // "…"; invariant ① never trips at ANY width (a degenerate W's fold is
1089
+ // a cut, never a crash).
1090
+ const room = W - 8 - keyW - visibleWidth(cut);
1091
+ if (room < 2)
1092
+ return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(meta, Math.max(1, W - 8 - visibleWidth(cut)))}…`];
1093
+ return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(meta, room)}…${key}`];
1032
1094
  }
1033
1095
  // ---- the bounded-block flow contract (W7, W8, W10) ----
1034
1096
  /** The caps — screen rows counted AFTER the fold, at the current width
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.16.6",
3
+ "version": "0.16.7",
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",