@vincemakes/kiso-tui-cells 0.16.8 → 0.17.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.
@@ -283,6 +283,86 @@ export declare function exploreRows(parts: readonly {
283
283
  /** Does this tool's fold term count distinct targets rather than calls? */
284
284
  export declare function foldCountsObjects(name: string): boolean;
285
285
  export declare function foldTerms(reads: number, edits: number, others: readonly [string, number][]): string[];
286
+ /**
287
+ * R3i — THE STRETCH LINE: the turn's one working row, in three phases.
288
+ *
289
+ * A STRETCH is the run of thinking and tool calls between two blocks of
290
+ * the model's prose. While it runs it is this line plus a bounded act
291
+ * window; when it closes it commits as this same line, frozen, with its
292
+ * key. The contract in one sentence: **the line you watch is the line
293
+ * you keep** — the settle changes the mark, the tense and the key, and
294
+ * nothing else.
295
+ *
296
+ * thinking ✧ thinking 4s
297
+ * acting ✶ reading 6 files · running 4 shell commands
298
+ * settled ✦ thought 9s · read 6 files · ran 4 shell commands · ctrl+r
299
+ *
300
+ * THE GIVE-WAY LADDER, in order, because at some width everything
301
+ * cannot fit:
302
+ *
303
+ * 1. the human's WORDS (the A9 chip on a quiet turn) — they are on
304
+ * screen above, in the chip band;
305
+ * 2. the NOUNS compact, cheapest word first, and stop as soon as the
306
+ * row fits — buying one cell must not spend every substitution;
307
+ * 3. the COUNTS cut, with the honest "…";
308
+ * 4. the TROUBLE CLAUSE cuts. The design first said it never gives
309
+ * way, and that was unimplementable: a long clause overflows after
310
+ * the counts have already cut to a bare "…", and invariant ①
311
+ * throws on that row;
312
+ * 5. the KEY gives way NEVER. A fold with no key is the turn's work
313
+ * behind a line with no way back to it, which is the one thing
314
+ * this row must not be.
315
+ *
316
+ * `…` in this file means CUT HERE and nothing else — which is why the
317
+ * live phases carry no trailing ellipsis for in-flight, though the
318
+ * reference implementation uses one. The moving mark and the present
319
+ * tense already say it twice.
320
+ */
321
+ /**
322
+ * R3i phase 5 — THE ANSWERED QUESTION'S BLOCK.
323
+ *
324
+ * A settled `ask_user` used to render ` ask_user (3 lines, 41.2s)` —
325
+ * an empty target and the answers discarded, though the tool_result
326
+ * already carried them. The owner asked for this block by pointing at
327
+ * one: after they answer, there is a display for that too.
328
+ *
329
+ * asked 2 questions (answered, 41.2s)
330
+ * │ deploy target → staging
331
+ * │ retry policy → give up after 3 attempts (typed)
332
+ *
333
+ * The question is dim, the join is dim, the ANSWER is at body strength
334
+ * — strip every escape and every fact is still there (law 1.2: colour
335
+ * is emphasis, never information). A typed answer says `(typed)`,
336
+ * because where an answer came from is a fact about it.
337
+ *
338
+ * It is WORDS, not work (law 1.7): it never folds into a stretch line,
339
+ * because the one thing a summary must not do is speak for the human.
340
+ *
341
+ * A result that is not the ask's own JSON yields NOTHING. This renderer
342
+ * reads a payload it did not write, and a guess about what it means
343
+ * would be a row the product cannot stand behind.
344
+ */
345
+ export declare function askedBlock(resultText: string, seconds: number, W: number): string[];
346
+ export interface StretchTerms {
347
+ /** the segment's OWN measured thinking seconds; 0 drops the term */
348
+ readonly thoughtSeconds: number;
349
+ /** the segment's calls as [tool name, count], in first-call order.
350
+ * Object-counting tools are deduped by target upstream (R3h). */
351
+ readonly calls: readonly (readonly [string, number])[];
352
+ /** the targets acted on — read only when the stretch made exactly
353
+ * ONE call, where naming the target says everything the two rows it
354
+ * replaces said (see the one-call rule below). */
355
+ readonly targets: readonly string[];
356
+ /** the trouble the stretch met: [kind, count, what it was]. */
357
+ readonly trouble: readonly (readonly ["failed" | "denied" | "interrupted", number, string])[];
358
+ /** A9 — the human's words, on a QUIET turn's fold only. */
359
+ readonly words?: string;
360
+ /** the live mark; the caller passes the spinner's current frame. */
361
+ readonly mark?: string;
362
+ }
363
+ export declare function stretchLine(t: StretchTerms & {
364
+ readonly phase: "thinking" | "acting" | "settled";
365
+ }, W: number): string[];
286
366
  export declare function turnFold(t: {
287
367
  words: string;
288
368
  thoughtSeconds: number;
@@ -593,6 +593,18 @@ class ToolExecution {
593
593
  return out;
594
594
  }
595
595
  if (c.state === "done") {
596
+ // R3i phase 5: an answered (or declined) ask_user renders its
597
+ // OWN block — the questions and what the human said. The row
598
+ // it replaces was ` ask_user (3 lines, 41.2s)`: an empty
599
+ // target and the answers thrown away, though the result
600
+ // already carried them. `askedBlock` returns [] for anything
601
+ // that is not the ask's own JSON, so a payload this renderer
602
+ // did not write can never be guessed at.
603
+ if (c.name === "ask_user" && c.reason === null && !c.isError) {
604
+ const asked = askedBlock(c.resultText, c.startedAt !== null && c.doneAt !== null ? (c.doneAt - c.startedAt) / 1000 : 0, W);
605
+ if (asked.length > 0)
606
+ return asked;
607
+ }
596
608
  // W19: the pinned deny — the claimed shape verbatim: the FULL
597
609
  // call name (the denial names the call), the target, the reason
598
610
  // in the W4 parentheses idiom, no timing (the call never ran).
@@ -1013,14 +1025,29 @@ function countTerm(n, singular, plural) {
1013
1025
  * commands". A tool with no entry says `3 × <verb>`, which counts calls
1014
1026
  * without inventing a noun for them.
1015
1027
  */
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"],
1028
+ /**
1029
+ * R3i — ONE TERM TABLE, TWO TENSES: [past, progressive, singular, plural].
1030
+ *
1031
+ * The stretch line is the same row at every instant of a turn: while
1032
+ * the work runs it says what it is DOING, and at the settle it says
1033
+ * what it DID. That sentence is only true if both tenses come from one
1034
+ * table. The v9 review found the alternative already happening on a
1035
+ * hand-written prototype — `searching 1 pattern` live against `ran 1
1036
+ * search` settled, the NOUN swapping at the settle, and `running 4
1037
+ * shells`, which is verbatim the R3g defect the previous round removed.
1038
+ *
1039
+ * FOLD_TERM below is derived from this, so the settled vocabulary
1040
+ * cannot drift from the live one by construction.
1041
+ */
1042
+ const TERM = {
1043
+ read_file: ["read", "reading", "file", "files"],
1044
+ edit_file: ["edited", "editing", "file", "files"],
1045
+ write_file: ["wrote", "writing", "file", "files"],
1046
+ list_dir: ["listed", "listing", "directory", "directories"],
1047
+ search_text: ["ran", "running", "search", "searches"],
1048
+ shell: ["ran", "running", "shell command", "shell commands"],
1023
1049
  };
1050
+ const FOLD_TERM = Object.fromEntries(Object.entries(TERM).map(([name, [past, , singular, plural]]) => [name, [past, singular, plural]]));
1024
1051
  /**
1025
1052
  * R3h (fable, 2026-08-29) — WHICH TERMS COUNT OBJECTS.
1026
1053
  *
@@ -1058,6 +1085,167 @@ export function foldTerms(reads, edits, others) {
1058
1085
  }
1059
1086
  return parts;
1060
1087
  }
1088
+ /**
1089
+ * R3i — THE STRETCH LINE: the turn's one working row, in three phases.
1090
+ *
1091
+ * A STRETCH is the run of thinking and tool calls between two blocks of
1092
+ * the model's prose. While it runs it is this line plus a bounded act
1093
+ * window; when it closes it commits as this same line, frozen, with its
1094
+ * key. The contract in one sentence: **the line you watch is the line
1095
+ * you keep** — the settle changes the mark, the tense and the key, and
1096
+ * nothing else.
1097
+ *
1098
+ * thinking ✧ thinking 4s
1099
+ * acting ✶ reading 6 files · running 4 shell commands
1100
+ * settled ✦ thought 9s · read 6 files · ran 4 shell commands · ctrl+r
1101
+ *
1102
+ * THE GIVE-WAY LADDER, in order, because at some width everything
1103
+ * cannot fit:
1104
+ *
1105
+ * 1. the human's WORDS (the A9 chip on a quiet turn) — they are on
1106
+ * screen above, in the chip band;
1107
+ * 2. the NOUNS compact, cheapest word first, and stop as soon as the
1108
+ * row fits — buying one cell must not spend every substitution;
1109
+ * 3. the COUNTS cut, with the honest "…";
1110
+ * 4. the TROUBLE CLAUSE cuts. The design first said it never gives
1111
+ * way, and that was unimplementable: a long clause overflows after
1112
+ * the counts have already cut to a bare "…", and invariant ①
1113
+ * throws on that row;
1114
+ * 5. the KEY gives way NEVER. A fold with no key is the turn's work
1115
+ * behind a line with no way back to it, which is the one thing
1116
+ * this row must not be.
1117
+ *
1118
+ * `…` in this file means CUT HERE and nothing else — which is why the
1119
+ * live phases carry no trailing ellipsis for in-flight, though the
1120
+ * reference implementation uses one. The moving mark and the present
1121
+ * tense already say it twice.
1122
+ */
1123
+ /**
1124
+ * R3i phase 5 — THE ANSWERED QUESTION'S BLOCK.
1125
+ *
1126
+ * A settled `ask_user` used to render ` ask_user (3 lines, 41.2s)` —
1127
+ * an empty target and the answers discarded, though the tool_result
1128
+ * already carried them. The owner asked for this block by pointing at
1129
+ * one: after they answer, there is a display for that too.
1130
+ *
1131
+ * asked 2 questions (answered, 41.2s)
1132
+ * │ deploy target → staging
1133
+ * │ retry policy → give up after 3 attempts (typed)
1134
+ *
1135
+ * The question is dim, the join is dim, the ANSWER is at body strength
1136
+ * — strip every escape and every fact is still there (law 1.2: colour
1137
+ * is emphasis, never information). A typed answer says `(typed)`,
1138
+ * because where an answer came from is a fact about it.
1139
+ *
1140
+ * It is WORDS, not work (law 1.7): it never folds into a stretch line,
1141
+ * because the one thing a summary must not do is speak for the human.
1142
+ *
1143
+ * A result that is not the ask's own JSON yields NOTHING. This renderer
1144
+ * reads a payload it did not write, and a guess about what it means
1145
+ * would be a row the product cannot stand behind.
1146
+ */
1147
+ export function askedBlock(resultText, seconds, W) {
1148
+ let parsed;
1149
+ try {
1150
+ parsed = JSON.parse(resultText);
1151
+ }
1152
+ catch {
1153
+ return [];
1154
+ }
1155
+ if (parsed === null || typeof parsed !== "object")
1156
+ return [];
1157
+ const asked = parsed;
1158
+ const p = palette();
1159
+ const head = (n, outcome) => cutLine(` ${p.bold}asked${p.reset} ${n} ${n === 1 ? "question" : "questions"} ${p.dim}(${outcome}, ${seconds.toFixed(1)}s)${p.reset}`, W);
1160
+ const row = (body) => cutLine(` ${p.dim}│${p.reset} ${body}`, W);
1161
+ if (Array.isArray(asked.declined) && asked.declined.length > 0) {
1162
+ // the honest decline record: WHAT went unanswered, and what the
1163
+ // choices had been — the panel already computes both.
1164
+ return [head(asked.declined.length, "declined"), ...asked.declined.map((q) => row(`${p.dim}${escapeTerminal(q)}${p.reset}`))];
1165
+ }
1166
+ if (!Array.isArray(asked.answers) || asked.answers.length === 0)
1167
+ return [];
1168
+ return [
1169
+ head(asked.answers.length, "answered"),
1170
+ ...asked.answers.map((a) => {
1171
+ const q = escapeTerminal(String(a.q ?? ""));
1172
+ const typed = typeof a.custom === "string" && a.custom !== "";
1173
+ const value = typed ? a.custom : Array.isArray(a.choices) ? a.choices.join(", ") : String(a.choice ?? "");
1174
+ return row(`${p.dim}${q} →${p.reset} ${escapeTerminal(value)}${typed ? `${p.dim} (typed)${p.reset}` : ""}`);
1175
+ }),
1176
+ ];
1177
+ }
1178
+ const STRETCH_COMPACT = [
1179
+ ["directories", "dirs"],
1180
+ ["directory", "dir"],
1181
+ ["shell commands", "commands"],
1182
+ ["shell command", "command"],
1183
+ ];
1184
+ /** R3i — a stretch of exactly ONE call names its TARGET instead of its
1185
+ * count. `thought 2s · read 1 file` replaces two rows — the thinking
1186
+ * and the call — with a row that says less than either of them did,
1187
+ * and "thinking plus one call" is the commonest shape a narrating
1188
+ * model makes. This is the answer to the defect R3d killed R3b's
1189
+ * per-segment folds over; the "absorbs at least two rows" rule alone
1190
+ * does not answer it. */
1191
+ function stretchTerms(t, live) {
1192
+ const total = t.calls.reduce((n, [, c]) => n + c, 0);
1193
+ if (total === 1 && t.targets.length === 1) {
1194
+ const [name] = t.calls[0];
1195
+ const e = TERM[name];
1196
+ return [`${e === undefined ? name : e[live ? 1 : 0]} ${t.targets[0]}`];
1197
+ }
1198
+ return t.calls
1199
+ .filter(([, n]) => n > 0)
1200
+ .map(([name, n]) => {
1201
+ const e = TERM[name];
1202
+ if (e === undefined)
1203
+ return `${n} × ${displayVerb(name)}`;
1204
+ return `${e[live ? 1 : 0]} ${n} ${n === 1 ? e[2] : e[3]}`;
1205
+ });
1206
+ }
1207
+ /** R3i — the trouble clause: which call, and what happened, in WORDS.
1208
+ * Law 1.3 says an outcome is stated in words, "the only form that
1209
+ * survives a pipe"; the colour on this clause is emphasis over those
1210
+ * words, never the fact itself. */
1211
+ function troubleClause(t) {
1212
+ return t.trouble
1213
+ .filter(([, n]) => n > 0)
1214
+ .map(([kind, n, what]) => (what === "" || kind === "interrupted" ? `${n} ${kind}` : `${n} ${kind}: ${what}`))
1215
+ .join(" · ");
1216
+ }
1217
+ export function stretchLine(t, W) {
1218
+ const p = palette();
1219
+ const live = t.phase !== "settled";
1220
+ const mark = t.phase === "settled" ? `${p.bold}✦${p.reset}` : `${p.dim}${t.mark ?? "✧"}${p.reset}`;
1221
+ const key = t.phase === "settled" ? " · ctrl+r" : "";
1222
+ const lead = t.phase === "thinking" ? [`thinking ${t.thoughtSeconds}s`] : t.phase === "settled" && t.thoughtSeconds > 0 ? [`thought ${t.thoughtSeconds}s`] : [];
1223
+ const clauseText = troubleClause(t);
1224
+ let meta = [...lead, ...(t.phase === "thinking" ? [] : stretchTerms(t, live))].join(" · ");
1225
+ let clause = clauseText === "" ? "" : ` · ${clauseText}`;
1226
+ let words = t.words === undefined || t.words === "" ? "" : ` ${escapeTerminal(t.words).replace(/\s+/g, " ")} `;
1227
+ const width = () => 2 + (words === "" ? 0 : visibleWidth(words) + 3) + visibleWidth(meta) + visibleWidth(clause) + visibleWidth(key);
1228
+ const trim = (text, room, floor) => (room >= floor ? `${widthCut(text, room)}…` : "");
1229
+ if (words !== "" && width() > W)
1230
+ words = trim(words, W - (width() - visibleWidth(words)) - 1, 4);
1231
+ if (width() > W) {
1232
+ for (const [long, short] of STRETCH_COMPACT) {
1233
+ meta = meta.replaceAll(long, short);
1234
+ if (width() <= W)
1235
+ break;
1236
+ }
1237
+ }
1238
+ if (width() > W) {
1239
+ const room = W - (width() - visibleWidth(meta)) - 1;
1240
+ meta = room >= 2 ? `${widthCut(meta, room)}…` : "…";
1241
+ }
1242
+ if (width() > W && clause !== "")
1243
+ clause = trim(clause, W - (width() - visibleWidth(clause)) - 1, 5);
1244
+ // the degenerate floor: below the width where even the mark and one
1245
+ // character fit, the row is a hard cut of what it would have said.
1246
+ 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}`}`;
1247
+ return [visibleWidth(row) <= W ? row : cutLine(row, W)];
1248
+ }
1061
1249
  export function turnFold(t, W) {
1062
1250
  const p = palette();
1063
1251
  // R3b (owner, 2026-08-27): ZERO TERMS ARE DROPPED. W14 always wrote
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.16.8",
3
+ "version": "0.17.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",