@vincemakes/kiso-tui 0.1.35 → 0.1.36

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.
@@ -188,8 +188,14 @@ class ThinkingFold {
188
188
  return [`${palette().dim}…${widthCut(trimmed, slice)}${suffix}${palette().reset}`];
189
189
  }
190
190
  }
191
- /** The tool execution line + the approval mini-diff — every state is
192
- * its own render; the lines fold (the summary gives way first). */
191
+ /** The tool execution line + the bounded block — every state is its
192
+ * own render; the lines fold (the summary gives way first). W7 (the
193
+ * flow contract): the block's BODY (the rows below the header) is
194
+ * capped in SCREEN rows AFTER the fold, at the current width — the
195
+ * renderer-cut row (`└ +N … · ctrl+r`) sits INSIDE the cap (a
196
+ * truncated block is cap−1 output rows + the cut row); the TOOL-cut
197
+ * row (`└ capped by …` — the tool's OWN truncation note, W10) is a
198
+ * DIFFERENT fact, never counted in the output cap. */
193
199
  class ToolExecution {
194
200
  cell;
195
201
  constructor(cell) {
@@ -205,29 +211,162 @@ class ToolExecution {
205
211
  const line = c.isError
206
212
  ? `${p.red}✗ ${name} (${escapeTerminal(c.resultText.split("\n")[0].slice(0, 60))}, ${elapsed}s)${p.reset}`
207
213
  : `${p.bold}✓ ${name}${p.reset} (${summary}${c.added + c.removed > 0 ? `, +${c.added} -${c.removed}` : ""}, ${elapsed}s)`;
208
- return foldLine(line, W);
214
+ const out = foldLine(line, W);
215
+ out.push(...toolBlockBody(c, W));
216
+ return out;
209
217
  }
210
218
  if (c.state === "approval") {
211
- const lines = foldLine(`→ ${name} ${summary} ${p.bold}⏸${p.reset}`, W);
212
- if (c.diff !== null) {
213
- for (const d of c.diff) {
214
- const body = d.kind === "-"
215
- ? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
216
- : d.kind === "+"
217
- ? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
218
- : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
219
- lines.push(...foldLine(`${p.bold}▎${p.reset}${body}`, W));
220
- }
221
- }
222
- return lines;
219
+ const out = foldLine(`→ ${name} ${summary} ${p.bold}⏸${p.reset}`, W);
220
+ out.push(...toolBlockBody(c, W));
221
+ return out;
223
222
  }
224
223
  if (c.state === "running") {
225
224
  const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
226
- return foldLine(`→ ${name} ${summary} ${p.bold}${SPINNER[ctx.spinnerI % SPINNER.length]}${p.reset} ${elapsed}s`, W);
225
+ const out = foldLine(`→ ${name} ${summary} ${p.bold}${SPINNER[ctx.spinnerI % SPINNER.length]}${p.reset} ${elapsed}s`, W);
226
+ out.push(...toolBlockBody(c, W));
227
+ return out;
227
228
  }
228
229
  return foldLine(`→ ${name} ${summary}`, W);
229
230
  }
230
231
  }
232
+ // ---- the bounded-block flow contract (W7, W8, W10) ----
233
+ /** The caps — screen rows counted AFTER the fold, at the current width
234
+ * (the W7 table). The renderer-cut row is inside the cap. */
235
+ const CAP_SHELL_SETTLED = 5; // the shell output tail, settled
236
+ const CAP_LIVE_WINDOW = 3; // the running tool's FIXED window (W8)
237
+ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
238
+ const CAP_ERROR = 3; // the error text head
239
+ /** The block body rows' prefixes (W2's gutter table): │ a bounded
240
+ * block's body, └ the block's last row — what was cut, where the rest
241
+ * is. Structural (constraint 1) — they survive a pipe. */
242
+ const BODY_ROW = " │ ";
243
+ const CUT_ROW = " └ ";
244
+ const blockMemo = new WeakMap();
245
+ /** The block's body rows below the header (memoized, W9). */
246
+ function toolBlockBody(c, W) {
247
+ const memo = blockMemo.get(c);
248
+ const state = `${c.state}:${c.isError}:${c.name}`;
249
+ const content = c.state === "approval" ? (c.diff ?? null) : c.resultText;
250
+ if (memo !== undefined && memo.width === W && memo.state === state && memo.content === content)
251
+ return memo.rows;
252
+ const p = palette();
253
+ const rows = c.state === "done"
254
+ ? c.isError
255
+ ? errorBody(c.resultText, W)
256
+ : c.name.startsWith("shell")
257
+ ? shellTail(c.resultText, W)
258
+ : []
259
+ : c.state === "running"
260
+ ? liveWindow(c.resultText, W)
261
+ : c.state === "approval"
262
+ ? diffBody(c.diff, W)
263
+ : [];
264
+ const note = toolCutNote(c.name, c.resultText);
265
+ if (note !== null)
266
+ rows.push(...foldLine(`${p.dim}${CUT_ROW}${note}${p.reset}`, W));
267
+ blockMemo.set(c, { width: W, state, content, rows });
268
+ return rows;
269
+ }
270
+ /** Fold result text into dim body rows (the BODY_ROW prefix): escape,
271
+ * split, fold each line at W−prefix; trailing empty rows (the result's
272
+ * final newline) drop. */
273
+ function blockRows(text, W) {
274
+ const p = palette();
275
+ const textW = Math.max(1, W - visibleWidth(BODY_ROW));
276
+ const rows = [];
277
+ for (const raw of escapeTerminal(text).split("\n")) {
278
+ for (const row of foldLine(raw, textW))
279
+ rows.push(`${p.dim}${BODY_ROW}${row}${p.reset}`);
280
+ }
281
+ while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(BODY_ROW))
282
+ rows.pop();
283
+ return rows;
284
+ }
285
+ /** The shell output tail, settled: the LAST rows, capped at 5 — the
286
+ * renderer cut at the block's bottom ("earlier rows" — the conclusion
287
+ * is at the end, pi's truncateToVisualLines direction). */
288
+ function shellTail(text, W) {
289
+ const p = palette();
290
+ const rows = blockRows(text, W);
291
+ if (rows.length <= CAP_SHELL_SETTLED)
292
+ return rows;
293
+ const kept = CAP_SHELL_SETTLED - 1;
294
+ const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - kept} earlier rows · ctrl+r${p.reset}`, W);
295
+ return [...rows.slice(rows.length - kept), ...cut];
296
+ }
297
+ /** The error text head: the FIRST rows, capped at 3 — the answer is at
298
+ * the start (opencode's collapseToolOutput direction). The header row
299
+ * already summarizes the first line, so the body starts at line 2. */
300
+ function errorBody(text, W) {
301
+ const p = palette();
302
+ const rows = blockRows(text.split("\n").slice(1).join("\n"), W);
303
+ if (rows.length <= CAP_ERROR)
304
+ return rows;
305
+ const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - (CAP_ERROR - 1)} more · ctrl+r${p.reset}`, W);
306
+ return [...rows.slice(0, CAP_ERROR - 1), ...cut];
307
+ }
308
+ /** The running tool's FIXED-height window (W8): exactly 3 rows from
309
+ * the FIRST frame — blank-padded before output arrives, the renderer
310
+ * cut inside the window. The height changes exactly once, at settle —
311
+ * a cell that grows mid-list would shift every row after it on every
312
+ * delta (the parallel-tools jitter). */
313
+ function liveWindow(text, W) {
314
+ const p = palette();
315
+ if (text === "") {
316
+ return [`${p.dim}${BODY_ROW}${p.reset}`, `${p.dim}${BODY_ROW}${p.reset}`, `${p.dim}${CUT_ROW}waiting for output${p.reset}`];
317
+ }
318
+ const rows = blockRows(text, W);
319
+ if (rows.length <= CAP_LIVE_WINDOW) {
320
+ while (rows.length < CAP_LIVE_WINDOW)
321
+ rows.push(`${p.dim}${BODY_ROW}${p.reset}`);
322
+ return rows;
323
+ }
324
+ const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - (CAP_LIVE_WINDOW - 1)} earlier rows · ctrl+r${p.reset}`, W);
325
+ return [...rows.slice(rows.length - (CAP_LIVE_WINDOW - 1)), ...cut];
326
+ }
327
+ /** The approval mini-diff (W7): capped at 12 folded rows — the head +
328
+ * the named middle (the renderer cut — what was cut, how to expand) +
329
+ * the tail. The rows are folded at the current width BEFORE the cap —
330
+ * the R1 measured bug: truncateDiff capped at 40 ENTRIES while the
331
+ * fold turned them into 73 SCREEN rows at W≤80 (a 44-row terminal's
332
+ * content cap is H−4 = 40 — the approval force-committed a third of
333
+ * the screen into scrollback inside one frame). */
334
+ function diffBody(diff, W) {
335
+ const p = palette();
336
+ if (diff === null)
337
+ return [];
338
+ const rows = [];
339
+ for (const d of diff) {
340
+ const body = d.kind === "-"
341
+ ? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
342
+ : d.kind === "+"
343
+ ? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
344
+ : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
345
+ rows.push(...foldLine(`${p.bold}▎${p.reset}${body}`, W));
346
+ }
347
+ if (rows.length <= CAP_DIFF)
348
+ return rows;
349
+ const head = Math.floor((CAP_DIFF - 1) / 2);
350
+ const tail = CAP_DIFF - 1 - head;
351
+ const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - head - tail} rows · ctrl+r to expand · /last for the full diff${p.reset}`, W);
352
+ return [...rows.slice(0, head), ...cut, ...rows.slice(rows.length - tail)];
353
+ }
354
+ /** The TOOL's OWN truncation note (W10) — a different fact from the
355
+ * renderer's cut: the tools truncate and append a continuation note
356
+ * (packages/tools-node/src/index.ts — read_file's "call again with
357
+ * offset=N", the output cap, list_dir's entry cap). The note reaches
358
+ * the MODEL and never the human — this row surfaces it. Detected in
359
+ * the result's TAIL (the note is appended at the end); returns null
360
+ * when the tool did not truncate. */
361
+ function toolCutNote(name, resultText) {
362
+ const tail = resultText.slice(-300);
363
+ const m = /offset=(\d+)/.exec(tail);
364
+ if (m !== null)
365
+ return `capped by ${escapeTerminal(name)} · offset=${m[1]} for the rest`;
366
+ if (/…\[truncated\]/.test(tail) || /… \+?\d+ more (?:lines|entries)/.test(tail))
367
+ return `capped by ${escapeTerminal(name)} · /last for the rest`;
368
+ return null;
369
+ }
231
370
  /** The assistant body text — wrapped at W, the inline-code tint per
232
371
  * row (the #16e rule: a span never matches across rows). */
233
372
  class AssistantMessage {
@@ -20,10 +20,13 @@
20
20
  * sharp edge (asserted by the VT-emulator gate);
21
21
  * - two crash invariants: ① every emitted line's visible width ≤ W
22
22
  * (components fold; a violation THROWS with diagnostics — pi
23
- * tui-main-screen.ts:447-473, no silent truncate); ② within the
24
- * live region only RELATIVE cursor moves (vertical A/B, horizontal
25
- * G/D) CUP exists only in the full-redraw path (the first frame,
26
- * the resize repaint);
23
+ * tui-main-screen.ts:447-473, no silent truncate); ② every steady-
24
+ * frame CUP lands in the CONTENT area (rows H−4−menu — the
25
+ * committed band, the stale/gap ELs, and the LIVE lines at their
26
+ * model rows; fix C's sanctioned reinterpretation, ADR-0046) — the
27
+ * CHROME rows (H−3..H) are RELATIVE-only (vertical A/B, horizontal
28
+ * G/D); CUP over the whole screen exists only in the full-redraw
29
+ * path (the first frame, the resize repaint);
27
30
  * - the cursor DERIVES from the frame: the focus component embeds the
28
31
  * APC marker in its rendered line; the compositor locates, strips,
29
32
  * and relatively positions from the frame — no side-channel cursor
@@ -20,10 +20,13 @@
20
20
  * sharp edge (asserted by the VT-emulator gate);
21
21
  * - two crash invariants: ① every emitted line's visible width ≤ W
22
22
  * (components fold; a violation THROWS with diagnostics — pi
23
- * tui-main-screen.ts:447-473, no silent truncate); ② within the
24
- * live region only RELATIVE cursor moves (vertical A/B, horizontal
25
- * G/D) CUP exists only in the full-redraw path (the first frame,
26
- * the resize repaint);
23
+ * tui-main-screen.ts:447-473, no silent truncate); ② every steady-
24
+ * frame CUP lands in the CONTENT area (rows H−4−menu — the
25
+ * committed band, the stale/gap ELs, and the LIVE lines at their
26
+ * model rows; fix C's sanctioned reinterpretation, ADR-0046) — the
27
+ * CHROME rows (H−3..H) are RELATIVE-only (vertical A/B, horizontal
28
+ * G/D); CUP over the whole screen exists only in the full-redraw
29
+ * path (the first frame, the resize repaint);
27
30
  * - the cursor DERIVES from the frame: the focus component embeds the
28
31
  * APC marker in its rendered line; the compositor locates, strips,
29
32
  * and relatively positions from the frame — no side-channel cursor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",