@vincemakes/kiso-tui-cells 0.16.7 → 0.16.8

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,6 +280,8 @@ export declare function exploreRows(parts: readonly {
280
280
  name: string;
281
281
  subjects: readonly string[];
282
282
  }[], W: number): string[];
283
+ /** Does this tool's fold term count distinct targets rather than calls? */
284
+ export declare function foldCountsObjects(name: string): boolean;
283
285
  export declare function foldTerms(reads: number, edits: number, others: readonly [string, number][]): string[];
284
286
  export declare function turnFold(t: {
285
287
  words: string;
@@ -920,7 +920,14 @@ export function exploreCounts(parts) {
920
920
  return parts
921
921
  .map((part) => {
922
922
  const [singular, plural] = EXPLORE_NOUN[part.name] ?? ["call", "calls"];
923
- return `${part.subjects.length} ${part.subjects.length === 1 ? singular : plural}`;
923
+ // R3h (fable, 2026-08-29): DISTINCT subjects. This counted calls
924
+ // while exploreRows — the very next function, the expansion of
925
+ // THIS row — deduped them with `×N`. So the head said "6 files"
926
+ // over a list showing four, one of them `a.ts ×3`. The head and
927
+ // the body are two views of one run and must count alike; the
928
+ // body was right (a file read twice is one file).
929
+ const n = foldCountsObjects(part.name) ? new Set(part.subjects).size : part.subjects.length;
930
+ return `${n} ${n === 1 ? singular : plural}`;
924
931
  })
925
932
  .join(" · ");
926
933
  }
@@ -965,12 +972,20 @@ function countTerm(n, singular, plural) {
965
972
  * as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
966
973
  * in, the verb + "s" otherwise).
967
974
  * A9 (ruling R2, mock A): the user chip rides the fold — the human's
968
- * words LEAD the one line, `▞ <chip> · thought 19s · 5 reads · no
969
- * edits` — the chip the SAME SGR-7 bracket as the live user row (#16f,
970
- * side pads included). The words take the fold's width budget: the
971
- * metadata terms survive (the W14 metadata rule they give way LAST),
972
- * the words width-cut at the end with the honest "…" (never a silent
973
- * truncate — invariant ① holds on the ONE row by construction). */
975
+ * words LEAD the one line, `✦ <chip> · thought 19s · read 5 files`
976
+ * the chip the SAME SGR-7 bracket as the live user row (#16f, side
977
+ * pads included). The words take the fold's width budget and width-cut
978
+ * at the end with the honest "…" (never a silent truncate invariant
979
+ * holds on the ONE row by construction).
980
+ *
981
+ * DECLARED SUPERSESSION (R3g, 2026-08-28) — A9 also ruled that "the
982
+ * metadata terms give way LAST". They do not any more: the KEY does.
983
+ * A9 was taken when this line carried no key, and at a width where the
984
+ * chip, the full metadata and " · ctrl+r" cannot coexist, a fold with
985
+ * no key is the turn's work behind a line with no way back to it. So
986
+ * the order is now words, then metadata, then — never — the key. The
987
+ * glyph is ✦ and the zero terms are dropped (R3b), so the example above
988
+ * is written as the code renders it rather than as A9 first wrote it. */
974
989
  /**
975
990
  * R3b — what a run of work DID, in words. One definition, because two
976
991
  * surfaces say it: the fold line (`turnFold`, above) and the expand
@@ -1006,6 +1021,24 @@ const FOLD_TERM = {
1006
1021
  search_text: ["ran", "search", "searches"],
1007
1022
  shell: ["ran", "shell command", "shell commands"],
1008
1023
  };
1024
+ /**
1025
+ * R3h (fable, 2026-08-29) — WHICH TERMS COUNT OBJECTS.
1026
+ *
1027
+ * The rule, one sentence: a term that counts OBJECTS counts distinct
1028
+ * objects; a term that counts ACTS counts calls. `read 2 files` after
1029
+ * reading ONE file twice is a false sentence, and law 1.3 does not
1030
+ * become optional because the falsehood is small. `ran 2 searches`
1031
+ * after searching the same pattern twice is TRUE — the acts happened.
1032
+ *
1033
+ * The table sits beside FOLD_TERM so the two cannot drift: a tool whose
1034
+ * noun is a thing ("files", "directories") belongs here; a tool whose
1035
+ * noun is an act ("searches", "shell commands") does not.
1036
+ */
1037
+ const FOLD_COUNTS_OBJECTS = new Set(["read_file", "edit_file", "write_file", "list_dir"]);
1038
+ /** Does this tool's fold term count distinct targets rather than calls? */
1039
+ export function foldCountsObjects(name) {
1040
+ return FOLD_COUNTS_OBJECTS.has(name);
1041
+ }
1009
1042
  function foldTerm(name, n) {
1010
1043
  const t = FOLD_TERM[name];
1011
1044
  if (t === undefined)
@@ -1032,7 +1065,12 @@ export function turnFold(t, W) {
1032
1065
  // happen — on a segment fold, where a run is usually all reads or all
1033
1066
  // edits, half the row was the half that said nothing. A term earns its
1034
1067
  // place by having a count.
1035
- const meta = [`thought ${t.thoughtSeconds}s`, ...foldTerms(t.reads, t.edits, t.others)].join(" · ");
1068
+ // R3h (fable, 2026-08-29): the THOUGHT term obeys the same zero-drop
1069
+ // rule every other term got at R3b. It was exempt by accident — it
1070
+ // was written before the rule — so a model that emits no thinking
1071
+ // folded every turn of its life under `thought 0s`, a sentence about
1072
+ // something that did not happen, in the lead position.
1073
+ const meta = [...(t.thoughtSeconds > 0 ? [`thought ${t.thoughtSeconds}s`] : []), ...foldTerms(t.reads, t.edits, t.others)].join(" · ");
1036
1074
  // R3f: the same one-row rule. This one is worse than the thinking
1037
1075
  // fold's — the fold is a COMMITTED row, so a multi-line user message
1038
1076
  // would desync the committed-line accounting permanently rather than
@@ -1056,6 +1094,29 @@ export function turnFold(t, W) {
1056
1094
  // words was unreachable by the key its siblings advertise. Found by
1057
1095
  // an independent review (fable) and then by the paced-rollup gate,
1058
1096
  // which got `kind: "none"` back from expandNext.
1097
+ // R3h: the COMPACT tier. The owner's own sentence — "thought 17s ·
1098
+ // read 4 files · listed 1 directory · ran 4 shell commands" — is 81
1099
+ // cells with the key at W=80: one over, so the tail was cut off the
1100
+ // exact shape this round exists to produce. The nouns shorten before
1101
+ // anything is lost, which is the same "degrade, never truncate"
1102
+ // ladder every other row here walks.
1103
+ // A LADDER, cheapest first, and it stops as soon as the row fits: the
1104
+ // owner's canonical sentence is ONE cell over at W=80, and spending
1105
+ // every substitution to buy one cell would shorten words that had
1106
+ // room. "directory" is the cheapest to lose (nobody misreads "dir");
1107
+ // "commands" is next and still says what happened.
1108
+ const COMPACT = [
1109
+ ["directories", "dirs"],
1110
+ ["directory", "dir"],
1111
+ ["shell commands", "commands"],
1112
+ ["shell command", "command"],
1113
+ ];
1114
+ const compactAll = (text) => {
1115
+ let out = text;
1116
+ for (const [long, short] of COMPACT)
1117
+ out = out.replaceAll(long, short);
1118
+ return out;
1119
+ };
1059
1120
  const KEY = " · ctrl+r";
1060
1121
  const keyW = KEY.length;
1061
1122
  const key = `${p.dim}${KEY}${p.reset}`;
@@ -1063,12 +1124,29 @@ export function turnFold(t, W) {
1063
1124
  const keyed = `${p.bold}✦${p.reset} ${meta}${key}`;
1064
1125
  if (visibleWidth(keyed) <= W)
1065
1126
  return [keyed];
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.
1127
+ let tightMeta = meta;
1128
+ for (const [long, short] of COMPACT) {
1129
+ tightMeta = tightMeta.replaceAll(long, short);
1130
+ const tight = `${p.bold}✦${p.reset} ${tightMeta}${key}`;
1131
+ if (visibleWidth(tight) <= W)
1132
+ return [tight];
1133
+ }
1134
+ // the meta cuts with the honest "…" — the COMPACT form of it, so a
1135
+ // cut row still carries as many whole terms as the width allows;
1136
+ // below the width where even the key fits there is nothing to
1137
+ // preserve and the row is a cut.
1068
1138
  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}`];
1139
+ if (room < 2) {
1140
+ // R3h: and below FOUR columns even that row is 4 cells wide (the
1141
+ // gutter, one character, the "…"), so invariant ① threw AT the
1142
+ // widths this branch exists to serve — the same class DC-15
1143
+ // closed in the thinking fold, still open here. A width with no
1144
+ // room for the mark is a width with nothing to say: the row is
1145
+ // a hard cut of what it would have said.
1146
+ const cut = `${p.bold}✦${p.reset} ${widthCut(tightMeta, Math.max(1, W - 3))}…`;
1147
+ return [visibleWidth(cut) <= W ? cut : cutLine(`${p.bold}✦${p.reset} ${tightMeta}`, W)];
1148
+ }
1149
+ return [`${p.bold}✦${p.reset} ${widthCut(tightMeta, room)}…${key}`];
1072
1150
  }
1073
1151
  // A9 (ruling R2, mock A): the user chip rides the fold — the human's
1074
1152
  // words LEAD the one line, the same SGR-7 bracket as the live user
@@ -1083,14 +1161,29 @@ export function turnFold(t, W) {
1083
1161
  const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
1084
1162
  if (visibleWidth(row) <= W)
1085
1163
  return [row];
1164
+ // R3h: the same COMPACT ladder the wordless branch walks — the nouns
1165
+ // shorten before the words or the metadata lose anything.
1166
+ let chipMeta = meta;
1167
+ for (const [long, short] of COMPACT) {
1168
+ chipMeta = chipMeta.replaceAll(long, short);
1169
+ const tighter = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${chipMeta}${key}`;
1170
+ if (visibleWidth(tighter) <= W)
1171
+ return [tighter];
1172
+ }
1086
1173
  // the last resort: the METADATA gives way — the words hold their
1087
1174
  // budget, the key holds its nine cells, the meta cuts with the honest
1088
1175
  // "…"; invariant ① never trips at ANY width (a degenerate W's fold is
1089
1176
  // a cut, never a crash).
1090
1177
  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}`];
1178
+ if (room < 2) {
1179
+ // R3h: the chip branch's own degenerate floor. The gutter, the
1180
+ // bracket's pads, the join and the "…" are ten cells before a
1181
+ // single character of content, so every width below ten threw
1182
+ // invariant ① — see the wordless branch above for the same class.
1183
+ const tail = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), Math.max(1, W - 8 - visibleWidth(cut)))}…`;
1184
+ return [visibleWidth(tail) <= W ? tail : cutLine(`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd}`, W)];
1185
+ }
1186
+ return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), room)}…${key}`];
1094
1187
  }
1095
1188
  // ---- the bounded-block flow contract (W7, W8, W10) ----
1096
1189
  /** 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.7",
3
+ "version": "0.16.8",
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",