@vincemakes/kiso-tui 0.1.28 → 0.1.30
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/body.d.ts +27 -6
- package/dist/body.js +114 -47
- package/dist/dock.js +9 -3
- package/dist/editor.js +3 -3
- package/dist/render.d.ts +16 -9
- package/dist/render.js +22 -11
- package/package.json +1 -1
package/dist/body.d.ts
CHANGED
|
@@ -62,6 +62,14 @@ export type BodyCell = {
|
|
|
62
62
|
label: string;
|
|
63
63
|
line: string;
|
|
64
64
|
done: true;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "checklist";
|
|
67
|
+
header: string;
|
|
68
|
+
items: {
|
|
69
|
+
text: string;
|
|
70
|
+
status: "pending" | "active" | "done";
|
|
71
|
+
}[];
|
|
72
|
+
done: true;
|
|
65
73
|
};
|
|
66
74
|
export interface BodyOptions {
|
|
67
75
|
/** Is the cell renderer live? A color TTY with a real size — checked
|
|
@@ -85,12 +93,17 @@ export declare class Body {
|
|
|
85
93
|
/** Teardown — flush a pending frame, stop the timers. */
|
|
86
94
|
close(): void;
|
|
87
95
|
/**
|
|
88
|
-
*
|
|
89
|
-
* rows
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
* #17 (P1): a resize reflows the terminal's buffer — the old chrome
|
|
97
|
+
* rows SURVIVE the reflow at their shifted positions (the recorded
|
|
98
|
+
* separator wall + the tail ghost — the #16a assumption that the
|
|
99
|
+
* reflow erases them was wrong). The handler: (1) clear the old tail +
|
|
100
|
+
* dock area with the OLD geometry (one ED from the last-drawn tail
|
|
101
|
+
* top; EL/ED only, zero LF — the #16 storm gate's invariants hold);
|
|
102
|
+
* (2) redraw immediately at the NEW geometry (the tail, the cursor
|
|
103
|
+
* home, the dock — via the normal render). The frozen content is
|
|
104
|
+
* strictly ABOVE the old tail top — the clear never touches it (the
|
|
105
|
+
* frozen bytes stay emitted exactly once). Consecutive resizes are
|
|
106
|
+
* idempotent: the clear covers an already-clear area.
|
|
94
107
|
*/
|
|
95
108
|
onResize(): void;
|
|
96
109
|
/** The last COMPLETE thinking block, for /think. */
|
|
@@ -121,6 +134,14 @@ export declare class Body {
|
|
|
121
134
|
/** The terminal's status line + the rhythm gap (one blank). */
|
|
122
135
|
terminal(label: string, statusLine: string): void;
|
|
123
136
|
notice(text: string): void;
|
|
137
|
+
/** ⑥ todo round: the durable checklist — header + one brick-glyph line
|
|
138
|
+
* per item, frozen immediately (it is static content). The CLI
|
|
139
|
+
* translates a tagged tool result into the structured items; the
|
|
140
|
+
* passthrough writes the same lines (byte-identical in pipes). */
|
|
141
|
+
checklist(header: string, items: {
|
|
142
|
+
text: string;
|
|
143
|
+
status: "pending" | "active" | "done";
|
|
144
|
+
}[]): void;
|
|
124
145
|
/** A pre-rendered block (the banner, the session line, slash-command
|
|
125
146
|
* outputs) — frozen immediately. */
|
|
126
147
|
raw(lines: string[]): void;
|
package/dist/body.js
CHANGED
|
@@ -36,8 +36,9 @@ export class Body {
|
|
|
36
36
|
#opts;
|
|
37
37
|
#cells = [];
|
|
38
38
|
#nextFrozen = 0; // index of the first not-yet-printed cell
|
|
39
|
-
#
|
|
40
|
-
#oldTailTop = 0; // the tail
|
|
39
|
+
#height = 0; // the last DRAWN height — the resize handler clears with the OLD geometry
|
|
40
|
+
#oldTailTop = 0; // the last-drawn tail top — the clear pass + the resize handler's clear
|
|
41
|
+
#oldTailHeight = 0; // the last-drawn tail's height — the clear covers EXACTLY it (never the frozen area)
|
|
41
42
|
#frameTimer = null;
|
|
42
43
|
#heartbeat = null;
|
|
43
44
|
#dirty = false;
|
|
@@ -53,14 +54,13 @@ export class Body {
|
|
|
53
54
|
this.#opts = opts;
|
|
54
55
|
this.#write = opts.write ?? ((s) => process.stdout.write(s));
|
|
55
56
|
this.#active = opts.active();
|
|
57
|
+
this.#height = opts.height();
|
|
56
58
|
if (this.#isActive()) {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// CUP rows — the overwrite garbage after a drag (the #16
|
|
63
|
-
// defect). The dock redraws its own chrome on the same event.
|
|
59
|
+
// #17 (P1): the resize handler — clear the OLD tail + dock area
|
|
60
|
+
// (old geometry, EL/ED only — no LF), then redraw at the new
|
|
61
|
+
// geometry. The #16a assumption is retired: the terminal's
|
|
62
|
+
// reflow does NOT erase the old chrome rows (the recorded
|
|
63
|
+
// separator wall + the tail ghost prove it) — the clear does.
|
|
64
64
|
this.#resizeHandler = () => this.onResize();
|
|
65
65
|
process.stdout.on("resize", this.#resizeHandler);
|
|
66
66
|
this.#heartbeat = setInterval(() => {
|
|
@@ -104,23 +104,31 @@ export class Body {
|
|
|
104
104
|
this.render();
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
* rows
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
107
|
+
* #17 (P1): a resize reflows the terminal's buffer — the old chrome
|
|
108
|
+
* rows SURVIVE the reflow at their shifted positions (the recorded
|
|
109
|
+
* separator wall + the tail ghost — the #16a assumption that the
|
|
110
|
+
* reflow erases them was wrong). The handler: (1) clear the old tail +
|
|
111
|
+
* dock area with the OLD geometry (one ED from the last-drawn tail
|
|
112
|
+
* top; EL/ED only, zero LF — the #16 storm gate's invariants hold);
|
|
113
|
+
* (2) redraw immediately at the NEW geometry (the tail, the cursor
|
|
114
|
+
* home, the dock — via the normal render). The frozen content is
|
|
115
|
+
* strictly ABOVE the old tail top — the clear never touches it (the
|
|
116
|
+
* frozen bytes stay emitted exactly once). Consecutive resizes are
|
|
117
|
+
* idempotent: the clear covers an already-clear area.
|
|
113
118
|
*/
|
|
114
119
|
onResize() {
|
|
115
120
|
if (!this.#isActive())
|
|
116
121
|
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
122
|
+
const from = this.#oldTailTop > 0 ? this.#oldTailTop : Math.max(1, this.#height - 3);
|
|
123
|
+
const H = this.#opts.height(); // the NEW height — the clamp for a shrunk screen
|
|
124
|
+
const out = [];
|
|
125
|
+
// The cursor home + the ED land with the NEW geometry — rows beyond
|
|
126
|
+
// the old screen are already gone; rows below the old tail top are
|
|
127
|
+
// exactly the tail + the dock areas (the old tail, the old chrome).
|
|
128
|
+
out.push(`\x1b[${Math.min(from, Math.max(1, H))};1H\x1b[0J`);
|
|
129
|
+
this.#write(out.join(""));
|
|
130
|
+
this.#dirty = true; // the immediate redraw at the NEW geometry
|
|
131
|
+
this.render();
|
|
124
132
|
}
|
|
125
133
|
/** The last COMPLETE thinking block, for /think. */
|
|
126
134
|
lastThinking() {
|
|
@@ -301,6 +309,26 @@ export class Body {
|
|
|
301
309
|
this.#cells.push({ kind: "notice", text, done: true });
|
|
302
310
|
this.#mark();
|
|
303
311
|
}
|
|
312
|
+
/** ⑥ todo round: the durable checklist — header + one brick-glyph line
|
|
313
|
+
* per item, frozen immediately (it is static content). The CLI
|
|
314
|
+
* translates a tagged tool result into the structured items; the
|
|
315
|
+
* passthrough writes the same lines (byte-identical in pipes). */
|
|
316
|
+
checklist(header, items) {
|
|
317
|
+
const glyphOf = (status) => (status === "pending" ? "□" : status === "active" ? "▖" : "▣");
|
|
318
|
+
if (!this.#isActive()) {
|
|
319
|
+
this.#closeOpenThinking();
|
|
320
|
+
this.#closeOpenText();
|
|
321
|
+
const p = palette();
|
|
322
|
+
process.stdout.write(`${p.bold}▞${p.reset} ${escapeTerminal(header)}\n`);
|
|
323
|
+
for (const item of items)
|
|
324
|
+
process.stdout.write(` ${glyphOf(item.status)} ${escapeTerminal(item.text)}\n`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.#closeOpenThinking();
|
|
328
|
+
this.#closeOpenText();
|
|
329
|
+
this.#cells.push({ kind: "checklist", header, items, done: true });
|
|
330
|
+
this.#mark();
|
|
331
|
+
}
|
|
304
332
|
/** A pre-rendered block (the banner, the session line, slash-command
|
|
305
333
|
* outputs) — frozen immediately. */
|
|
306
334
|
raw(lines) {
|
|
@@ -344,45 +372,63 @@ export class Body {
|
|
|
344
372
|
const W = this.#opts.width();
|
|
345
373
|
if (H < 4)
|
|
346
374
|
return;
|
|
375
|
+
this.#height = H;
|
|
347
376
|
const out = [];
|
|
348
|
-
// #13 (P1), v2d-B: NO DECSTBM —
|
|
349
|
-
// the screen's last row
|
|
350
|
-
// NATIVE scrollback
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
377
|
+
// #13 (P1), v2d-B: NO DECSTBM — a frozen line scrolls with a REAL
|
|
378
|
+
// LF at the screen's last row (\x1b[H;1H\n — the top line leaves
|
|
379
|
+
// into the NATIVE scrollback) and lands at the body's bottom row,
|
|
380
|
+
// just above the active tail. The dock is redrawn after.
|
|
381
|
+
// #17 (P1): the pre-fill phase is GONE — EVERY frozen line takes
|
|
382
|
+
// this REAL-LF path, short sessions included. The old pre-fill drew
|
|
383
|
+
// at absolute CUP rows with NO real LF: the terminal's resize
|
|
384
|
+
// reflow treated those rows as soft lines — the recorded fold/body
|
|
385
|
+
// MERGE (the /think suffix lost), the tail ghost, the separator
|
|
386
|
+
// wall. A real-LF line reflows as ONE logical line, never merged
|
|
387
|
+
// with a neighbor; the only soft lines left are the active tail +
|
|
388
|
+
// the dock (small, redrawn every frame / cleared by onResize).
|
|
356
389
|
// The tail (the remaining ACTIVE cells) and its geometry — computed
|
|
357
390
|
// FIRST from the final nextFrozen, so the frozen cells are NOT in it
|
|
358
391
|
// (a stale tail would re-draw them — the double-render).
|
|
359
392
|
let nextFrozen = this.#nextFrozen;
|
|
360
393
|
while (nextFrozen < this.#cells.length && this.#cells[nextFrozen].done)
|
|
361
394
|
nextFrozen += 1;
|
|
395
|
+
// #17: the tail holds ONLY the unfinished cells — slice(nextFrozen)
|
|
396
|
+
// also captures the DONE cells that follow (an approval verdict
|
|
397
|
+
// freezing behind a live text+tool), inflating tailHeight so the
|
|
398
|
+
// tail's top rises INTO the frozen area and its clear pass wipes
|
|
399
|
+
// the freshly-frozen lines (the fold's /think suffix — recorded).
|
|
362
400
|
const tail = this.#cells.slice(nextFrozen);
|
|
363
|
-
|
|
401
|
+
// #17: the tail HEIGHT counts only the unfinished cells — slice()
|
|
402
|
+
// also captures the DONE cells that follow (an approval verdict
|
|
403
|
+
// freezing behind a live text+tool); counting them inflates the
|
|
404
|
+
// height so the tail's top rises INTO the frozen area and its
|
|
405
|
+
// clear pass wipes the freshly-frozen lines (the fold's /think
|
|
406
|
+
// suffix — recorded). The tail array itself keeps the old shape.
|
|
407
|
+
const tailHeight = tail.reduce((n, c) => n + (c.done ? 0 : this.#cellHeight(c, W)), 0);
|
|
364
408
|
const tailTop = Math.max(1, H - 3 - tailHeight); // v3 §03: 4 dock rows below
|
|
365
409
|
const writeRow = Math.max(1, tailTop - 1); // the frozen area's bottom row
|
|
366
410
|
let scrolled = 0;
|
|
367
411
|
for (let i = this.#nextFrozen; i < nextFrozen; i += 1) {
|
|
368
412
|
for (const line of this.#cellLines(this.#cells[i], W)) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
out.push(`\x1b[${H};1H\n`); // the REAL LF — the whole screen scrolls
|
|
375
|
-
out.push(`\x1b[${writeRow};1H\x1b[0K${line}`);
|
|
376
|
-
scrolled += 1;
|
|
377
|
-
}
|
|
413
|
+
out.push(`\x1b[${H};1H\n`); // the REAL LF — the whole screen scrolls
|
|
414
|
+
out.push(`\x1b[${writeRow};1H\x1b[0K${line}`);
|
|
415
|
+
scrolled += 1;
|
|
378
416
|
}
|
|
379
417
|
this.#nextFrozen += 1;
|
|
380
418
|
}
|
|
381
|
-
// 2. the active tail — clear its old area (shifted up by the
|
|
382
|
-
// scrolls) and the current area, draw the cells at the body's
|
|
419
|
+
// 2. the active tail — clear EXACTLY its old area (shifted up by the
|
|
420
|
+
// freeze scrolls) and the current area, draw the cells at the body's
|
|
421
|
+
// bottom. #17: the old code cleared clearFrom..H-4 unconditionally —
|
|
422
|
+
// harmless when the frozen lines sat at the TOP (pre-fill), but with
|
|
423
|
+
// the real-LF commits the frozen lines land just ABOVE the tail, so
|
|
424
|
+
// an over-wide clear wipes the freshly-frozen cells (the recorded
|
|
425
|
+
// fold/response vanishing).
|
|
383
426
|
out.push("\x1b[?2026h");
|
|
427
|
+
const oldBottom = this.#oldTailHeight > 0 ? this.#oldTailTop + this.#oldTailHeight - 1 - scrolled : -1;
|
|
428
|
+
const newBottom = tailHeight > 0 ? tailTop + tailHeight - 1 : -1;
|
|
384
429
|
const clearFrom = Math.min(this.#oldTailTop === 0 ? tailTop : this.#oldTailTop - scrolled, tailTop);
|
|
385
|
-
|
|
430
|
+
const clearTo = Math.max(oldBottom, newBottom);
|
|
431
|
+
for (let row = clearFrom; row <= Math.min(clearTo, H - 4); row += 1) {
|
|
386
432
|
out.push(`\x1b[${row};1H\x1b[0K`);
|
|
387
433
|
}
|
|
388
434
|
let row = tailTop;
|
|
@@ -392,6 +438,8 @@ export class Body {
|
|
|
392
438
|
row += 1;
|
|
393
439
|
}
|
|
394
440
|
}
|
|
441
|
+
this.#oldTailTop = tailTop; // the last-drawn tail top — the resize clear starts here
|
|
442
|
+
this.#oldTailHeight = tailHeight;
|
|
395
443
|
// 3. the cursor home — the input line's edit column.
|
|
396
444
|
out.push(`\x1b[${H};${this.#opts.editCol()}H`);
|
|
397
445
|
out.push("\x1b[?2026l");
|
|
@@ -409,14 +457,22 @@ export class Body {
|
|
|
409
457
|
// rail — a bright-white BOLD ▍ per line, then the text (the
|
|
410
458
|
// reverse-video block is RETIRED: it washed out on light
|
|
411
459
|
// themes). Multi-line whole: every line carries the rail
|
|
412
|
-
// (
|
|
460
|
+
// (coherent across lines); resize-safe; NO_COLOR → the rail renders plain.
|
|
413
461
|
return cell.text.split("\n").map((l) => `${p.bold}▍${p.reset} ${escapeTerminal(l)}`);
|
|
414
462
|
case "thinking": {
|
|
415
463
|
const block = cell.text;
|
|
416
464
|
const trimmed = escapeTerminal(block.trim());
|
|
417
465
|
if (trimmed.length <= 100)
|
|
418
466
|
return [`${p.dim}…${trimmed}${p.reset}`];
|
|
419
|
-
|
|
467
|
+
const suffix = ` (${block.length} chars · /think)`;
|
|
468
|
+
// #17: the fold must FIT its row — every frozen line commits
|
|
469
|
+
// via the REAL-LF scroll path at the SAME write row; a
|
|
470
|
+
// soft-wrapped fold's continuation row would be clobbered by
|
|
471
|
+
// the next line's commit write (the /think suffix lost — the
|
|
472
|
+
// recorded symptom). The slice shrinks with the width; the
|
|
473
|
+
// suffix always rides the fold's own row.
|
|
474
|
+
const slice = Math.max(1, W - 1 - suffix.length);
|
|
475
|
+
return [`${p.dim}…${trimmed.slice(0, slice)}${suffix}${p.reset}`];
|
|
420
476
|
}
|
|
421
477
|
case "tool": {
|
|
422
478
|
const name = escapeTerminal(cell.name);
|
|
@@ -458,7 +514,7 @@ export class Body {
|
|
|
458
514
|
// TUI v5 #16e: the inline-code tint — backtick spans in
|
|
459
515
|
// assistant body text, matched PER LINE after the wrap (a
|
|
460
516
|
// span opened on one line and closed on another does NOT
|
|
461
|
-
// match —
|
|
517
|
+
// match — no cross-line matching). NO_COLOR → the codes are empty →
|
|
462
518
|
// byte-identical.
|
|
463
519
|
return wrapped.length > 0 ? wrapped.map((l) => colorInlineCode(l)) : [""];
|
|
464
520
|
}
|
|
@@ -470,7 +526,7 @@ export class Body {
|
|
|
470
526
|
// SGR is applied at COMPOSITION time (renderRecap/startupBanner),
|
|
471
527
|
// and model/tool content was already escapeTerminal'd there.
|
|
472
528
|
// Re-escaping at render STRIPPED the ESC from the SGR — the
|
|
473
|
-
// literal "[38;5;75m▞[0m" garbage the user saw (the #16
|
|
529
|
+
// literal "[38;5;75m▞[0m" garbage the user saw (the #16 mojibake,
|
|
474
530
|
// also the banner's dim). Verbatim: the injection guard lives
|
|
475
531
|
// at composition, not here.
|
|
476
532
|
return cell.lines;
|
|
@@ -478,6 +534,17 @@ export class Body {
|
|
|
478
534
|
// the honest label (done / aborted / error) + the status + the
|
|
479
535
|
// rhythm gap blank
|
|
480
536
|
return [cell.label, cell.line, ""];
|
|
537
|
+
case "checklist": {
|
|
538
|
+
// ⑥: the durable checklist — the ▞ header accent + one brick
|
|
539
|
+
// glyph per item (□ pending / ▖ active / ▣ done). Text is
|
|
540
|
+
// escaped at composition; the glyphs are renderer-owned.
|
|
541
|
+
const lines = [`${p.bold}▞${p.reset} ${escapeTerminal(cell.header)}`];
|
|
542
|
+
for (const item of cell.items) {
|
|
543
|
+
const glyph = item.status === "pending" ? "□" : item.status === "active" ? "▖" : "▣";
|
|
544
|
+
lines.push(` ${glyph} ${escapeTerminal(item.text)}`);
|
|
545
|
+
}
|
|
546
|
+
return lines;
|
|
547
|
+
}
|
|
481
548
|
}
|
|
482
549
|
}
|
|
483
550
|
#cellHeight(cell, W) {
|
package/dist/dock.js
CHANGED
|
@@ -141,14 +141,20 @@ export class Dock {
|
|
|
141
141
|
if (!this.#active)
|
|
142
142
|
return;
|
|
143
143
|
const p = palette();
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// #17 (P1): read the LIVE size, not the cache — the body's resize
|
|
145
|
+
// render calls onDock (this redraw) BEFORE this dock's own resize
|
|
146
|
+
// handler runs, so the cached geometry would draw the chrome at
|
|
147
|
+
// stale rows (clamped into the body — the separator residue wall).
|
|
148
|
+
// The live read makes the handler order irrelevant; the cache keeps
|
|
149
|
+
// serving exit().
|
|
150
|
+
const H = process.stdout.rows ?? this.#height;
|
|
151
|
+
const W = process.stdout.columns ?? this.#width;
|
|
146
152
|
const sep = `${p.dim}${"╌".repeat(W)}${p.reset}`;
|
|
147
153
|
const status = `${this.#status}${this.#tail === "" ? "" : ` · ${this.#tail}`}`;
|
|
148
154
|
const statusLine = this.#question ?? this.#statusRow(status, p, W);
|
|
149
155
|
const inp = this.#inputState();
|
|
150
156
|
const out = [];
|
|
151
|
-
// P3 (
|
|
157
|
+
// P3 (review): the DEC private-mode SET/RESET needs the "?" prefix —
|
|
152
158
|
// \x1b[?2026h/l, the pi source's exact form. Without it terminals
|
|
153
159
|
// silently ignore the mode and the anti-flicker never engages.
|
|
154
160
|
out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
|
package/dist/editor.js
CHANGED
|
@@ -102,7 +102,7 @@ export class Editor {
|
|
|
102
102
|
#onRender;
|
|
103
103
|
#menuOpen = false; // v3 §04: the slash-command menu
|
|
104
104
|
#menuSel = 0;
|
|
105
|
-
// A2 (
|
|
105
|
+
// A2 (the feel): the session-scoped input history — every submitted TURN
|
|
106
106
|
// line (never a question answer), capped at 100, never persisted. ↑↓
|
|
107
107
|
// navigate it ONLY from an empty input or while already browsing.
|
|
108
108
|
#history = [];
|
|
@@ -337,7 +337,7 @@ export class Editor {
|
|
|
337
337
|
}
|
|
338
338
|
else if (final === "A" || final === "B") {
|
|
339
339
|
// v3 §04: the menu owns ↑↓ while open (the selection, never the
|
|
340
|
-
// cursor). A2 (
|
|
340
|
+
// cursor). A2 (the feel): otherwise ↑↓ navigate the session history
|
|
341
341
|
// — ONLY from an empty input or while already browsing; mid-edit
|
|
342
342
|
// the cursor semantics are unchanged (↑↓ do nothing).
|
|
343
343
|
if (this.#menuOpen) {
|
|
@@ -427,7 +427,7 @@ export class Editor {
|
|
|
427
427
|
#submit() {
|
|
428
428
|
let line = String.fromCodePoint(...this.#chars);
|
|
429
429
|
if (this.#menuOpen) {
|
|
430
|
-
// A1 (
|
|
430
|
+
// A1 (the feel): Enter submits the EXACT selection directly; a
|
|
431
431
|
// PARTIAL selection COMPLETES the buffer (the Tab semantics)
|
|
432
432
|
// without submitting — the user reviews and presses Enter
|
|
433
433
|
// again. The old behavior executed the completed command on
|
package/dist/render.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* input, produce the lines a human sees. Colors are raw ANSI — no
|
|
4
4
|
* dependencies.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
|
|
7
7
|
* kiso-core's Event — the CLI translates Event → RenderInput. The tui
|
|
8
8
|
* package has ZERO kiso-core imports: input is data, output is bytes.
|
|
9
9
|
*/
|
|
@@ -31,7 +31,7 @@ export declare const COLOR_ON: Palette;
|
|
|
31
31
|
export declare const COLOR_OFF: Palette;
|
|
32
32
|
export declare function palette(): Palette;
|
|
33
33
|
/**
|
|
34
|
-
* E
|
|
34
|
+
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
35
35
|
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
36
36
|
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
37
37
|
* EVERY externally-sourced string must pass through this before any output.
|
|
@@ -56,7 +56,7 @@ export interface RenderResult {
|
|
|
56
56
|
readonly prompt: boolean;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* the ergonomics batch C5 — the render input, the tui's OWN data shape: the subset of
|
|
60
60
|
* an event stream the renderer reads, keyed by type. The CLI translates
|
|
61
61
|
* its Event stream into this (Event → RenderInput) before rendering —
|
|
62
62
|
* the tui package never imports kiso-core. Field names mirror the
|
|
@@ -137,12 +137,19 @@ export type RenderInput = {
|
|
|
137
137
|
readonly name: string;
|
|
138
138
|
readonly executionId: string;
|
|
139
139
|
readonly error: string;
|
|
140
|
+
} | {
|
|
141
|
+
readonly type: "checklist";
|
|
142
|
+
readonly header: string;
|
|
143
|
+
readonly items: readonly {
|
|
144
|
+
readonly text: string;
|
|
145
|
+
readonly status: "pending" | "active" | "done";
|
|
146
|
+
}[];
|
|
140
147
|
};
|
|
141
148
|
/**
|
|
142
149
|
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
143
150
|
* current line); `newline` says whether the line is complete.
|
|
144
151
|
*
|
|
145
|
-
*
|
|
152
|
+
* bootstrap P1: `prevThinking` marks a thinking delta that continues the SAME
|
|
146
153
|
* block — it renders appended to the segment, without the … prefix. The
|
|
147
154
|
* consumer closes the segment with a newline at the next non-thinking
|
|
148
155
|
* event.
|
|
@@ -159,7 +166,7 @@ export declare function foldThinking(block: string): string;
|
|
|
159
166
|
export declare function foldResult(content: string): string;
|
|
160
167
|
export declare function renderEvent(ev: RenderInput, prevThinking?: boolean, resolvePath?: PathResolver): RenderResult;
|
|
161
168
|
/**
|
|
162
|
-
* B
|
|
169
|
+
* B area: one-line summary of a completed tool call, e.g.
|
|
163
170
|
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
164
171
|
* ✗ shell npm test (exit 1)
|
|
165
172
|
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
@@ -171,7 +178,7 @@ export declare function renderToolSummary(name: string, input: Record<string, un
|
|
|
171
178
|
}): string;
|
|
172
179
|
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
173
180
|
export declare function kUnit(value: number | null): string;
|
|
174
|
-
/** B
|
|
181
|
+
/** B area: usage data gathered from the run's usage events. */
|
|
175
182
|
export interface RunUsage {
|
|
176
183
|
readonly in: number | null;
|
|
177
184
|
readonly out: number | null;
|
|
@@ -179,9 +186,9 @@ export interface RunUsage {
|
|
|
179
186
|
readonly known: boolean;
|
|
180
187
|
}
|
|
181
188
|
/**
|
|
182
|
-
* B
|
|
189
|
+
* B area/v2a: the one-line status bar after a terminal, e.g.
|
|
183
190
|
* [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
|
|
184
|
-
*
|
|
191
|
+
* Denoising: unknown fields are OMITTED ENTIRELY (show what there is); a fully unknown
|
|
185
192
|
* usage → null (the caller prints nothing); faux mode → [turn N · faux].
|
|
186
193
|
* All data comes from usage events; ctx is the approximate estimate
|
|
187
194
|
* passed in (chars/4 vs the window), marked with ~.
|
|
@@ -189,7 +196,7 @@ export interface RunUsage {
|
|
|
189
196
|
export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio: number, faux?: boolean): string | null;
|
|
190
197
|
/**
|
|
191
198
|
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
192
|
-
* hugs the terminal (
|
|
199
|
+
* hugs the terminal (show what there is — omitted when there is nothing to show),
|
|
193
200
|
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
194
201
|
* this verbatim; the render tests pin the sequence.
|
|
195
202
|
*/
|
package/dist/render.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* input, produce the lines a human sees. Colors are raw ANSI — no
|
|
4
4
|
* dependencies.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
|
|
7
7
|
* kiso-core's Event — the CLI translates Event → RenderInput. The tui
|
|
8
8
|
* package has ZERO kiso-core imports: input is data, output is bytes.
|
|
9
9
|
*/
|
|
@@ -13,7 +13,7 @@ export function palette() {
|
|
|
13
13
|
return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* E
|
|
16
|
+
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
17
17
|
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
18
18
|
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
19
19
|
* EVERY externally-sourced string must pass through this before any output.
|
|
@@ -44,7 +44,7 @@ export function colorInlineCode(line) {
|
|
|
44
44
|
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
45
45
|
* current line); `newline` says whether the line is complete.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
47
|
+
* bootstrap P1: `prevThinking` marks a thinking delta that continues the SAME
|
|
48
48
|
* block — it renders appended to the segment, without the … prefix. The
|
|
49
49
|
* consumer closes the segment with a newline at the next non-thinking
|
|
50
50
|
* event.
|
|
@@ -80,7 +80,7 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
|
80
80
|
case "text_end":
|
|
81
81
|
return { text: "\n", newline: true, prompt: false };
|
|
82
82
|
case "thinking":
|
|
83
|
-
//
|
|
83
|
+
// bootstrap P1/v2b: the CONSUMER buffers each thinking block and folds
|
|
84
84
|
// it to ONE dim line (foldThinking); this render is the generic
|
|
85
85
|
// path for tests. The full block goes to /think.
|
|
86
86
|
return {
|
|
@@ -112,7 +112,7 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
case "permission_requested":
|
|
115
|
-
//
|
|
115
|
+
// round 8: the tool NAME is model text — escaped like everything else.
|
|
116
116
|
return {
|
|
117
117
|
text: `⏸ ${escapeTerminal(ev.name)} needs approval ${p.dim}${approvalDetail(ev.name, ev.input, resolvePath)}${p.reset} `,
|
|
118
118
|
newline: false,
|
|
@@ -146,12 +146,23 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
|
146
146
|
newline: true,
|
|
147
147
|
prompt: false,
|
|
148
148
|
};
|
|
149
|
+
case "checklist": {
|
|
150
|
+
// ⑥: the durable checklist — the ▞ header accent (the recap's
|
|
151
|
+
// brick) + one brick-glyph line per item. Static line content:
|
|
152
|
+
// byte-identical in pipes and NO_COLOR.
|
|
153
|
+
const lines = [`${p.bold}▞${p.reset} ${escapeTerminal(ev.header)}`];
|
|
154
|
+
for (const item of ev.items) {
|
|
155
|
+
const glyph = item.status === "pending" ? "□" : item.status === "active" ? "▖" : "▣";
|
|
156
|
+
lines.push(` ${glyph} ${escapeTerminal(item.text)}`);
|
|
157
|
+
}
|
|
158
|
+
return { text: `${lines.join("\n")}\n`, newline: true, prompt: false };
|
|
159
|
+
}
|
|
149
160
|
default:
|
|
150
161
|
return { text: "", newline: false, prompt: false };
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
/**
|
|
154
|
-
* The approval prompt detail (Area 5
|
|
165
|
+
* The approval prompt detail (Area 5/round 8): the human must be able to see
|
|
155
166
|
* EVERYTHING they are approving. The shell command is shown in full; the
|
|
156
167
|
* path is the CANONICAL one the tool will touch; write/edit show the FULL
|
|
157
168
|
* content (never a truncated tail that hides a dangerous payload). The
|
|
@@ -171,7 +182,7 @@ function approvalDetail(name, input, resolvePath) {
|
|
|
171
182
|
return `\n ${escapeTerminal(JSON.stringify(input))}`;
|
|
172
183
|
}
|
|
173
184
|
/**
|
|
174
|
-
* B
|
|
185
|
+
* B area: one-line summary of a completed tool call, e.g.
|
|
175
186
|
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
176
187
|
* ✗ shell npm test (exit 1)
|
|
177
188
|
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
@@ -237,9 +248,9 @@ export function kUnit(value) {
|
|
|
237
248
|
return String(value);
|
|
238
249
|
}
|
|
239
250
|
/**
|
|
240
|
-
* B
|
|
251
|
+
* B area/v2a: the one-line status bar after a terminal, e.g.
|
|
241
252
|
* [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
|
|
242
|
-
*
|
|
253
|
+
* Denoising: unknown fields are OMITTED ENTIRELY (show what there is); a fully unknown
|
|
243
254
|
* usage → null (the caller prints nothing); faux mode → [turn N · faux].
|
|
244
255
|
* All data comes from usage events; ctx is the approximate estimate
|
|
245
256
|
* passed in (chars/4 vs the window), marked with ~.
|
|
@@ -264,7 +275,7 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
|
|
|
264
275
|
}
|
|
265
276
|
/**
|
|
266
277
|
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
267
|
-
* hugs the terminal (
|
|
278
|
+
* hugs the terminal (show what there is — omitted when there is nothing to show),
|
|
268
279
|
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
269
280
|
* this verbatim; the render tests pin the sequence.
|
|
270
281
|
*/
|
|
@@ -334,6 +345,6 @@ export function renderRecap(s) {
|
|
|
334
345
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
335
346
|
export function renderSessionLine(meta) {
|
|
336
347
|
const when = meta.updatedAt ? new Date(meta.updatedAt).toISOString().slice(0, 16) : "—";
|
|
337
|
-
//
|
|
348
|
+
// round 8: the title is the user's first prompt — model/user text, escaped.
|
|
338
349
|
return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
|
|
339
350
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
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",
|