@vincemakes/kiso-tui-cells 0.16.6 → 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.
- package/dist/components.d.ts +2 -24
- package/dist/components.js +188 -33
- package/package.json +1 -1
package/dist/components.d.ts
CHANGED
|
@@ -280,30 +280,8 @@ export declare function exploreRows(parts: readonly {
|
|
|
280
280
|
name: string;
|
|
281
281
|
subjects: readonly string[];
|
|
282
282
|
}[], W: number): string[];
|
|
283
|
-
/**
|
|
284
|
-
|
|
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
|
-
*/
|
|
283
|
+
/** Does this tool's fold term count distinct targets rather than calls? */
|
|
284
|
+
export declare function foldCountsObjects(name: string): boolean;
|
|
307
285
|
export declare function foldTerms(reads: number, edits: number, others: readonly [string, number][]): string[];
|
|
308
286
|
export declare function turnFold(t: {
|
|
309
287
|
words: string;
|
package/dist/components.js
CHANGED
|
@@ -289,7 +289,18 @@ class ThinkingFold {
|
|
|
289
289
|
render(W, _ctx) {
|
|
290
290
|
const p = palette();
|
|
291
291
|
const block = this.cell.text;
|
|
292
|
-
|
|
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,
|
|
@@ -909,7 +920,14 @@ export function exploreCounts(parts) {
|
|
|
909
920
|
return parts
|
|
910
921
|
.map((part) => {
|
|
911
922
|
const [singular, plural] = EXPLORE_NOUN[part.name] ?? ["call", "calls"];
|
|
912
|
-
|
|
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}`;
|
|
913
931
|
})
|
|
914
932
|
.join(" · ");
|
|
915
933
|
}
|
|
@@ -954,12 +972,20 @@ function countTerm(n, singular, plural) {
|
|
|
954
972
|
* as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
|
|
955
973
|
* in, the verb + "s" otherwise).
|
|
956
974
|
* A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
957
|
-
* words LEAD the one line,
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
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. */
|
|
963
989
|
/**
|
|
964
990
|
* R3b — what a run of work DID, in words. One definition, because two
|
|
965
991
|
* surfaces say it: the fold line (`turnFold`, above) and the expand
|
|
@@ -971,22 +997,64 @@ function countTerm(n, singular, plural) {
|
|
|
971
997
|
* Zero terms are dropped (owner ruling, R3b): a term earns its place by
|
|
972
998
|
* having a count.
|
|
973
999
|
*/
|
|
1000
|
+
/**
|
|
1001
|
+
* R3g (2026-08-28) — the fold's own terms, VERB + COUNT + NOUN.
|
|
1002
|
+
*
|
|
1003
|
+
* DECLARED SUPERSESSION. R3b built these terms out of ROLLUP_NOUN,
|
|
1004
|
+
* whose own comment says not to: that table names what a single-tool
|
|
1005
|
+
* rollup COUNTS ("5 matches" — five matched lines), and this line
|
|
1006
|
+
* counts CALLS. So one search_text call rendered `1 match`, a sentence
|
|
1007
|
+
* that is false whenever the search matched any other number — which is
|
|
1008
|
+
* almost always. `shell` fell through to the verb branch and read
|
|
1009
|
+
* `4 shells`.
|
|
1010
|
+
*
|
|
1011
|
+
* The phrasing is the owner's, from the shape they asked for:
|
|
1012
|
+
* "thought 17s · read 4 files · listed 1 directory · ran 4 shell
|
|
1013
|
+
* commands". A tool with no entry says `3 × <verb>`, which counts calls
|
|
1014
|
+
* without inventing a noun for them.
|
|
1015
|
+
*/
|
|
1016
|
+
const FOLD_TERM = {
|
|
1017
|
+
read_file: ["read", "file", "files"],
|
|
1018
|
+
edit_file: ["edited", "file", "files"],
|
|
1019
|
+
write_file: ["wrote", "file", "files"],
|
|
1020
|
+
list_dir: ["listed", "directory", "directories"],
|
|
1021
|
+
search_text: ["ran", "search", "searches"],
|
|
1022
|
+
shell: ["ran", "shell command", "shell commands"],
|
|
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
|
+
}
|
|
1042
|
+
function foldTerm(name, n) {
|
|
1043
|
+
const t = FOLD_TERM[name];
|
|
1044
|
+
if (t === undefined)
|
|
1045
|
+
return `${n} × ${displayVerb(name)}`;
|
|
1046
|
+
return `${t[0]} ${n} ${n === 1 ? t[1] : t[2]}`;
|
|
1047
|
+
}
|
|
974
1048
|
export function foldTerms(reads, edits, others) {
|
|
975
1049
|
const parts = [];
|
|
976
1050
|
if (reads > 0)
|
|
977
|
-
parts.push(
|
|
1051
|
+
parts.push(foldTerm("read_file", reads));
|
|
978
1052
|
if (edits > 0)
|
|
979
|
-
parts.push(
|
|
1053
|
+
parts.push(foldTerm("edit_file", edits));
|
|
980
1054
|
for (const [name, n] of others) {
|
|
981
1055
|
if (n === 0)
|
|
982
1056
|
continue;
|
|
983
|
-
|
|
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
|
-
}
|
|
1057
|
+
parts.push(foldTerm(name, n));
|
|
990
1058
|
}
|
|
991
1059
|
return parts;
|
|
992
1060
|
}
|
|
@@ -997,20 +1065,88 @@ export function turnFold(t, W) {
|
|
|
997
1065
|
// happen — on a segment fold, where a run is usually all reads or all
|
|
998
1066
|
// edits, half the row was the half that said nothing. A term earns its
|
|
999
1067
|
// place by having a count.
|
|
1000
|
-
|
|
1001
|
-
|
|
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(" · ");
|
|
1074
|
+
// R3f: the same one-row rule. This one is worse than the thinking
|
|
1075
|
+
// fold's — the fold is a COMMITTED row, so a multi-line user message
|
|
1076
|
+
// would desync the committed-line accounting permanently rather than
|
|
1077
|
+
// for one frame. (The live UserMessage chip keeps its multi-row form:
|
|
1078
|
+
// it folds per paragraph and each row is already one row.)
|
|
1079
|
+
const words = escapeTerminal(t.words).replace(/\s+/g, " ");
|
|
1080
|
+
// R3g (2026-08-28) — THE KEY IS LOAD-BEARING, SO IT GIVES WAY LAST.
|
|
1081
|
+
//
|
|
1082
|
+
// R3b wrote "the suffix gives way first at a narrow width", by
|
|
1083
|
+
// analogy with the settled card's affordance. The analogy does not
|
|
1084
|
+
// hold: the card's rows are still on the screen when its hint is
|
|
1085
|
+
// dropped, and a fold's are not. A fold with no key is the turn's
|
|
1086
|
+
// work behind a line with no way back — the one thing the fold's own
|
|
1087
|
+
// header says it must never be. So the META gives way first (a cut
|
|
1088
|
+
// count is still a true count), and the key survives.
|
|
1089
|
+
//
|
|
1090
|
+
// Two live cases, both reachable at W=80: the wordless fold went
|
|
1091
|
+
// keyless as soon as the terms grew (R3g's verb+noun phrasing pushed
|
|
1092
|
+
// it over), and the CHIP fold never had a key at all — it was
|
|
1093
|
+
// written without one, so every folded turn that carried the user's
|
|
1094
|
+
// words was unreachable by the key its siblings advertise. Found by
|
|
1095
|
+
// an independent review (fable) and then by the paced-rollup gate,
|
|
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
|
+
};
|
|
1120
|
+
const KEY = " · ctrl+r";
|
|
1121
|
+
const keyW = KEY.length;
|
|
1122
|
+
const key = `${p.dim}${KEY}${p.reset}`;
|
|
1002
1123
|
if (words === "") {
|
|
1003
|
-
const
|
|
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}`;
|
|
1124
|
+
const keyed = `${p.bold}✦${p.reset} ${meta}${key}`;
|
|
1011
1125
|
if (visibleWidth(keyed) <= W)
|
|
1012
1126
|
return [keyed];
|
|
1013
|
-
|
|
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.
|
|
1138
|
+
const room = W - 2 - keyW - 1;
|
|
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}`];
|
|
1014
1150
|
}
|
|
1015
1151
|
// A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
1016
1152
|
// words LEAD the one line, the same SGR-7 bracket as the live user
|
|
@@ -1020,15 +1156,34 @@ export function turnFold(t, W) {
|
|
|
1020
1156
|
// metadata's own width — the metadata survives, the words width-cut
|
|
1021
1157
|
// at the end with the honest "…" (the "…" alone is the honest floor:
|
|
1022
1158
|
// the words were there, cut).
|
|
1023
|
-
const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6);
|
|
1159
|
+
const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6 - keyW);
|
|
1024
1160
|
const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
|
|
1025
|
-
const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}`;
|
|
1161
|
+
const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
|
|
1026
1162
|
if (visibleWidth(row) <= W)
|
|
1027
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
|
+
}
|
|
1028
1173
|
// the last resort: the METADATA gives way — the words hold their
|
|
1029
|
-
// budget, the meta cuts with the honest
|
|
1030
|
-
// at ANY width (a degenerate W's fold is
|
|
1031
|
-
|
|
1174
|
+
// budget, the key holds its nine cells, the meta cuts with the honest
|
|
1175
|
+
// "…"; invariant ① never trips at ANY width (a degenerate W's fold is
|
|
1176
|
+
// a cut, never a crash).
|
|
1177
|
+
const room = W - 8 - keyW - visibleWidth(cut);
|
|
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}`];
|
|
1032
1187
|
}
|
|
1033
1188
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1034
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.
|
|
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",
|