@vincemakes/kiso-tui 0.1.34 → 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.
- package/dist/components.js +160 -17
- package/dist/compositor.d.ts +7 -4
- package/dist/compositor.js +49 -26
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -177,15 +177,25 @@ class ThinkingFold {
|
|
|
177
177
|
render(W, _ctx) {
|
|
178
178
|
const block = this.cell.text;
|
|
179
179
|
const trimmed = escapeTerminal(block.trim());
|
|
180
|
+
// the SHORT branch is width-aware TOO: the ≤100 short-circuit was
|
|
181
|
+
// W-blind — a short block at a narrow width returned the line
|
|
182
|
+
// UNFOLDED and tripped invariant ① (the crash class still live on
|
|
183
|
+
// npm for short /think blocks after a resize).
|
|
180
184
|
if (trimmed.length <= 100)
|
|
181
|
-
return [`${palette().dim}…${trimmed}${palette().reset}`];
|
|
185
|
+
return [`${palette().dim}…${widthCut(trimmed, Math.max(1, W - 1))}${palette().reset}`];
|
|
182
186
|
const suffix = ` (${block.length} chars · /think)`;
|
|
183
187
|
const slice = Math.max(1, W - 1 - suffix.length);
|
|
184
188
|
return [`${palette().dim}…${widthCut(trimmed, slice)}${suffix}${palette().reset}`];
|
|
185
189
|
}
|
|
186
190
|
}
|
|
187
|
-
/** The tool execution line + the
|
|
188
|
-
*
|
|
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. */
|
|
189
199
|
class ToolExecution {
|
|
190
200
|
cell;
|
|
191
201
|
constructor(cell) {
|
|
@@ -201,29 +211,162 @@ class ToolExecution {
|
|
|
201
211
|
const line = c.isError
|
|
202
212
|
? `${p.red}✗ ${name} (${escapeTerminal(c.resultText.split("\n")[0].slice(0, 60))}, ${elapsed}s)${p.reset}`
|
|
203
213
|
: `${p.bold}✓ ${name}${p.reset} (${summary}${c.added + c.removed > 0 ? `, +${c.added} -${c.removed}` : ""}, ${elapsed}s)`;
|
|
204
|
-
|
|
214
|
+
const out = foldLine(line, W);
|
|
215
|
+
out.push(...toolBlockBody(c, W));
|
|
216
|
+
return out;
|
|
205
217
|
}
|
|
206
218
|
if (c.state === "approval") {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const body = d.kind === "-"
|
|
211
|
-
? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
|
|
212
|
-
: d.kind === "+"
|
|
213
|
-
? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
|
|
214
|
-
: `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
|
|
215
|
-
lines.push(...foldLine(`${p.bold}▎${p.reset}${body}`, W));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return lines;
|
|
219
|
+
const out = foldLine(`→ ${name} ${summary} ${p.bold}⏸${p.reset}`, W);
|
|
220
|
+
out.push(...toolBlockBody(c, W));
|
|
221
|
+
return out;
|
|
219
222
|
}
|
|
220
223
|
if (c.state === "running") {
|
|
221
224
|
const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
|
|
222
|
-
|
|
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;
|
|
223
228
|
}
|
|
224
229
|
return foldLine(`→ ${name} ${summary}`, W);
|
|
225
230
|
}
|
|
226
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
|
+
}
|
|
227
370
|
/** The assistant body text — wrapped at W, the inline-code tint per
|
|
228
371
|
* row (the #16e rule: a span never matches across rows). */
|
|
229
372
|
class AssistantMessage {
|
package/dist/compositor.d.ts
CHANGED
|
@@ -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); ②
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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/dist/compositor.js
CHANGED
|
@@ -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); ②
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
@@ -674,17 +677,16 @@ export class Body {
|
|
|
674
677
|
#drawSteady(out, W, H, liveTop, liveLines, menuRows, ctx) {
|
|
675
678
|
const editor = this.#inputRow(W, ctx); // derived from the frame — the marker
|
|
676
679
|
const committed = this.#committedLinesThisFrame;
|
|
677
|
-
// the
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
out.push("\
|
|
687
|
-
}
|
|
680
|
+
// the jump from the anchor (H−2) straight to the bottom row H,
|
|
681
|
+
// then N real LFs scroll the screen exactly N rows — ONE per
|
|
682
|
+
// committed line (the bookkeeping; the stale 1B anchor jumped to
|
|
683
|
+
// H−1 and the N LFs scrolled only N−1 — the committed section sat
|
|
684
|
+
// one row short in the scrollback). The bottom-up repaint below
|
|
685
|
+
// overwrites the scrolled-in rows (the scroll count is screen-
|
|
686
|
+
// neutral — proven by the emulator probe).
|
|
687
|
+
out.push("\x1b[2B");
|
|
688
|
+
for (let i = 0; i < committed.length; i += 1)
|
|
689
|
+
out.push("\n");
|
|
688
690
|
// the bottom-up repaint, from the last row up — V6-3: the design
|
|
689
691
|
// §03 chrome: status (H), lower ╌ (H−1), input (H−2), upper ╌ (H−3)
|
|
690
692
|
out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`); // H — the status
|
|
@@ -694,17 +696,26 @@ export class Body {
|
|
|
694
696
|
for (let i = menuRows.length - 1; i >= 0; i -= 1) {
|
|
695
697
|
out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(menuRows[i], W)}`);
|
|
696
698
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
+
// the LIVE lines at their MODEL rows — CUP, never the relative
|
|
700
|
+
// march: the march drew them adjacent to the chrome (rows
|
|
701
|
+
// H−3−n..H−3) while the model placed them at [liveTop..liveTop+n−1]
|
|
702
|
+
// — in the unclamped geometry the gap ELs below erased the march's
|
|
703
|
+
// copy and the streamed text was INVISIBLE until the commit frame.
|
|
704
|
+
// The CUP rows land in the content area (≤ H−4−menu ≤ 21), inside
|
|
705
|
+
// the frozen CUP budget of invariant ②.
|
|
706
|
+
for (let i = 0; i < liveLines.length; i += 1) {
|
|
707
|
+
out.push(`\x1b[${liveTop + i};1H\x1b[0K${this.#checked(liveLines[i], W)}`);
|
|
699
708
|
}
|
|
700
709
|
// the FROZEN area — CUP (absolute rows; this is the FREEZE path —
|
|
701
|
-
// the old code's frozen writes were CUP too. The
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
710
|
+
// the old code's frozen writes were CUP too. The frozen rows are
|
|
711
|
+
// computed from the current geometry, so external writes (the CLI's
|
|
712
|
+
// console.error CRLF) cannot misplace them the way a relative march
|
|
713
|
+
// could).
|
|
705
714
|
// 1. the GAP rows (between the live content and the chrome) — EL'd
|
|
706
|
-
// so the old content there cannot ghost
|
|
707
|
-
|
|
715
|
+
// so the old content there cannot ghost; the range stops ABOVE
|
|
716
|
+
// the menu (the menu's rows at [H−3−menu..H−4] are marched and
|
|
717
|
+
// must survive — the unclamped geometry erased them).
|
|
718
|
+
for (let r = liveTop + liveLines.length; r <= H - 4 - menuRows.length; r += 1) {
|
|
708
719
|
out.push(`\x1b[${r};1H\x1b[0K`);
|
|
709
720
|
}
|
|
710
721
|
// 2. the STALE rows above the committed section — the scrolled old
|
|
@@ -721,9 +732,21 @@ export class Body {
|
|
|
721
732
|
out.push(`\x1b[${Math.max(1, liveTop - committed.length + i)};1H\x1b[0K${this.#checked(committed[i], W)}`);
|
|
722
733
|
}
|
|
723
734
|
// the cursor: down to the anchor (H−2, the input row) + left to the marker —
|
|
724
|
-
// the down-distance from the LAST written row
|
|
725
|
-
//
|
|
726
|
-
|
|
735
|
+
// the down-distance from the LAST written row, in byte order: the
|
|
736
|
+
// committed band's bottom, then the stale ELs' bottom, then the gap
|
|
737
|
+
// ELs' bottom, then the live lines' bottom, then the menu's top
|
|
738
|
+
// (its last marched row), else the chrome's upper ╌.
|
|
739
|
+
const lastRow = committed.length > 0
|
|
740
|
+
? Math.max(1, liveTop - 1)
|
|
741
|
+
: staleFrom < liveTop
|
|
742
|
+
? liveTop - 1
|
|
743
|
+
: liveTop + liveLines.length <= H - 4 - menuRows.length
|
|
744
|
+
? H - 4 - menuRows.length
|
|
745
|
+
: liveLines.length > 0
|
|
746
|
+
? liveTop + liveLines.length - 1
|
|
747
|
+
: menuRows.length > 0
|
|
748
|
+
? H - 3 - menuRows.length
|
|
749
|
+
: H - 3;
|
|
727
750
|
const down = H - 2 - lastRow; // the anchor: the input row (H−2)
|
|
728
751
|
if (down > 0)
|
|
729
752
|
out.push(`\x1b[${down}B`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.1.
|
|
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",
|