@vincemakes/kiso-tui 0.1.36 → 0.1.37
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 +26 -3
- package/dist/components.js +259 -46
- package/dist/compositor.d.ts +17 -3
- package/dist/compositor.js +104 -35
- package/dist/editor.d.ts +2 -6
- package/dist/editor.js +11 -53
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.d.ts +35 -4
- package/dist/render.js +115 -29
- package/dist/width.d.ts +15 -0
- package/dist/width.js +59 -0
- package/package.json +1 -1
package/dist/compositor.js
CHANGED
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { truncateDiff } from "./diff.js";
|
|
44
44
|
import { displayWidth } from "./editor.js";
|
|
45
|
-
import { Container, SPINNER, cellComponent, foldLine, footerLine, statusLine, visibleWidth, } from "./components.js";
|
|
46
|
-
import { escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary } from "./render.js";
|
|
45
|
+
import { Container, SPINNER, bodySpacing, cellComponent, foldLine, footerLine, statusLine, visibleWidth, } from "./components.js";
|
|
46
|
+
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary } from "./render.js";
|
|
47
47
|
/** The cursor marker — an APC private sequence the focus component
|
|
48
48
|
* embeds at the edit position; the compositor strips it and moves
|
|
49
49
|
* relatively (it never reaches the terminal). */
|
|
@@ -78,6 +78,7 @@ export class Body {
|
|
|
78
78
|
#resizeHandler = null;
|
|
79
79
|
// the chrome state (the Dock façade)
|
|
80
80
|
#status = "";
|
|
81
|
+
#statusHint = null;
|
|
81
82
|
#tail = "";
|
|
82
83
|
#question = null;
|
|
83
84
|
#inputState = () => ({ line: "", cursor: 0 });
|
|
@@ -154,8 +155,18 @@ export class Body {
|
|
|
154
155
|
this.#write(`→ ${escapeTerminal(name)}(${escapeTerminal(JSON.stringify(input).slice(0, 200))})\n`);
|
|
155
156
|
return;
|
|
156
157
|
}
|
|
158
|
+
// W12: the cell carries the delegate's child roles from the FULL
|
|
159
|
+
// input — the display summary is sliced at 60 chars (unparseable);
|
|
160
|
+
// the roles are the only running-state data the parent holds (there
|
|
161
|
+
// is no live channel to a running child session).
|
|
162
|
+
const childRoles = [];
|
|
163
|
+
for (const t of Array.isArray(input.tasks) ? input.tasks : []) {
|
|
164
|
+
if (typeof t === "object" && t !== null && typeof t.role === "string") {
|
|
165
|
+
childRoles.push(t.role);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
157
168
|
this.#toolCells.set(callId, this.#cells.length);
|
|
158
|
-
this.#cells.push({ kind: "tool", name, input: summary, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false });
|
|
169
|
+
this.#cells.push({ kind: "tool", name, input: summary, childRoles, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false });
|
|
159
170
|
this.#mark();
|
|
160
171
|
}
|
|
161
172
|
toolApproval(callId, diff) {
|
|
@@ -286,6 +297,30 @@ export class Body {
|
|
|
286
297
|
this.#cells.push({ kind: "checklist", header, items, done: true });
|
|
287
298
|
this.#mark();
|
|
288
299
|
}
|
|
300
|
+
/** The startup banner (W1): a LIVE cell — the tier re-derives per
|
|
301
|
+
* frame (bannerLines with the CURRENT W and H), so a resize re-tiers
|
|
302
|
+
* the art instead of re-folding frozen rows (a window below 40 cols
|
|
303
|
+
* never paints the logo). W5: the resume metas ride the cell — the
|
|
304
|
+
* list re-gates with the tier (BIG only) and re-times with the
|
|
305
|
+
* frame. The inactive path keeps the historical bytes (no resume —
|
|
306
|
+
* the pipe contract). */
|
|
307
|
+
banner(version, extensionsText, resume = []) {
|
|
308
|
+
if (!this.#isActive()) {
|
|
309
|
+
this.#closeOpenThinking();
|
|
310
|
+
this.#closeOpenText();
|
|
311
|
+
const W = this.#opts.width() || 80; // a 0-size pty falls back
|
|
312
|
+
const H = this.#opts.height();
|
|
313
|
+
const p = palette();
|
|
314
|
+
for (const r of bannerLines(W, H, version, extensionsText))
|
|
315
|
+
this.#write(`${p.dim}${r}${p.reset}\n`);
|
|
316
|
+
this.#write("\n");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.#closeOpenThinking();
|
|
320
|
+
this.#closeOpenText();
|
|
321
|
+
this.#cells.push({ kind: "banner", version, extensionsText, resume, done: true });
|
|
322
|
+
this.#mark();
|
|
323
|
+
}
|
|
289
324
|
raw(lines) {
|
|
290
325
|
if (!this.#isActive()) {
|
|
291
326
|
this.#closeOpenThinking();
|
|
@@ -360,8 +395,12 @@ export class Body {
|
|
|
360
395
|
this.#dirty = true;
|
|
361
396
|
this.render(); // the immediate redraw at the NEW geometry
|
|
362
397
|
}
|
|
363
|
-
|
|
398
|
+
/** W18: the status row's right-aligned hint is part of the status
|
|
399
|
+
* state — the compacting row passes "esc to cancel" (the affordance
|
|
400
|
+
* must survive repaints). */
|
|
401
|
+
setStatus(text, hint = null) {
|
|
364
402
|
this.#status = text;
|
|
403
|
+
this.#statusHint = hint;
|
|
365
404
|
this.redraw();
|
|
366
405
|
}
|
|
367
406
|
setTail(tail) {
|
|
@@ -452,19 +491,29 @@ export class Body {
|
|
|
452
491
|
}
|
|
453
492
|
// ---- the one writer ----
|
|
454
493
|
/** The live region's scalar — the unit tests assert the cap directly
|
|
455
|
-
* (the e2e gate pins the screen consequence).
|
|
494
|
+
* (the e2e gate pins the screen consequence). W11: the formula's
|
|
495
|
+
* blanks are join artifacts — the count includes them (they are real
|
|
496
|
+
* screen rows), threaded against the previous sibling's OWN rows. */
|
|
456
497
|
liveCount() {
|
|
457
498
|
const live = this.#cells.slice(this.#committed);
|
|
458
|
-
const ctx = { spinnerI: this.#spinnerI, now: Date.now() };
|
|
499
|
+
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
459
500
|
const W = this.#opts.width();
|
|
460
501
|
let lines = 0;
|
|
461
|
-
|
|
462
|
-
|
|
502
|
+
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
503
|
+
for (const cell of live) {
|
|
504
|
+
const rows = cellComponent(cell).render(W, ctx);
|
|
505
|
+
lines += bodySpacing(prev, rows).length;
|
|
506
|
+
prev = rows;
|
|
507
|
+
}
|
|
463
508
|
return lines + CHROME_ROWS + this.#menuRows(W).length;
|
|
464
509
|
}
|
|
465
510
|
/** The lines committed THIS frame — the writes land in the frame's
|
|
466
511
|
* committed section (the rows just above the live region). */
|
|
467
512
|
#committedLinesThisFrame = [];
|
|
513
|
+
// the CELL count when the frame began — the drawFull frozen bound's
|
|
514
|
+
// unit (the cells committed BEFORE this frame; a force-commit frame's
|
|
515
|
+
// placed LINES outnumber its cells)
|
|
516
|
+
#committedAtFrameStart = 0;
|
|
468
517
|
render() {
|
|
469
518
|
if (!this.#isActive())
|
|
470
519
|
return;
|
|
@@ -473,7 +522,7 @@ export class Body {
|
|
|
473
522
|
if (H < 4)
|
|
474
523
|
return;
|
|
475
524
|
this.#lastH = H;
|
|
476
|
-
const ctx = { spinnerI: this.#spinnerI, now: Date.now() };
|
|
525
|
+
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
477
526
|
// V6-1 (the screen-state == frame-state rule): the resize's first
|
|
478
527
|
// frame — the terminal's reflow re-wrapped the committed content at
|
|
479
528
|
// the NEW width, so the cached folds are stale. Re-fold the
|
|
@@ -485,8 +534,8 @@ export class Body {
|
|
|
485
534
|
for (let i = 0; i < this.#committed; i += 1) {
|
|
486
535
|
const cell = this.#cells[i];
|
|
487
536
|
const lines = cellComponent(cell).render(W, ctx);
|
|
488
|
-
this.#lineCache[i] = lines;
|
|
489
|
-
this.#committedLines += lines.length;
|
|
537
|
+
this.#lineCache[i] = lines; // the cell's OWN rows — the cache stays raw
|
|
538
|
+
this.#committedLines += bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines).length;
|
|
490
539
|
}
|
|
491
540
|
}
|
|
492
541
|
// 1. the natural commits — the leading DONE cells freeze: their
|
|
@@ -495,16 +544,24 @@ export class Body {
|
|
|
495
544
|
// short sessions included — the frame coalescing keeps a
|
|
496
545
|
// cell's first frame its freeze frame, so the frozen bytes
|
|
497
546
|
// emit exactly once).
|
|
547
|
+
this.#committedAtFrameStart = this.#committed;
|
|
498
548
|
this.#committedLinesThisFrame = [];
|
|
499
549
|
while (this.#committed < this.#cells.length && this.#cells[this.#committed].done) {
|
|
500
550
|
this.#commitCell(this.#committed, W, ctx);
|
|
501
551
|
}
|
|
502
552
|
// 2. the live lines — the unfinished cells (the tail) + the chrome.
|
|
553
|
+
// W11: the formula's blank above the first live cell hangs off
|
|
554
|
+
// the last COMMITTED sibling (the join spans the boundary).
|
|
503
555
|
const menuRows = this.#menuRows(W);
|
|
504
556
|
const chromeRows = CHROME_ROWS + menuRows.length;
|
|
505
557
|
let liveLines = [];
|
|
506
|
-
|
|
507
|
-
|
|
558
|
+
{
|
|
559
|
+
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
560
|
+
for (const cell of this.#cells.slice(this.#committed)) {
|
|
561
|
+
const rows = cellComponent(cell).render(W, ctx);
|
|
562
|
+
liveLines.push(...bodySpacing(prev, rows));
|
|
563
|
+
prev = rows;
|
|
564
|
+
}
|
|
508
565
|
}
|
|
509
566
|
// 3. the FORCE commits — the live region's hard cap H−1: overflow
|
|
510
567
|
// commits the oldest live cell UNCONDITIONALLY (the one sharp
|
|
@@ -512,8 +569,13 @@ export class Body {
|
|
|
512
569
|
while (liveLines.length > H - 4 && this.#committed < this.#cells.length) { // V6-3: the content cap H−4
|
|
513
570
|
this.#commitCell(this.#committed, W, ctx);
|
|
514
571
|
liveLines = [];
|
|
515
|
-
|
|
516
|
-
|
|
572
|
+
{
|
|
573
|
+
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
574
|
+
for (const cell of this.#cells.slice(this.#committed)) {
|
|
575
|
+
const rows = cellComponent(cell).render(W, ctx);
|
|
576
|
+
liveLines.push(...bodySpacing(prev, rows));
|
|
577
|
+
prev = rows;
|
|
578
|
+
}
|
|
517
579
|
}
|
|
518
580
|
}
|
|
519
581
|
// 4. the geometry — the live region's first row:
|
|
@@ -539,14 +601,17 @@ export class Body {
|
|
|
539
601
|
/** Commit the cell at index i: render + cache its lines (immutable —
|
|
540
602
|
* the force-committed form freezes at the current render), advance
|
|
541
603
|
* the bookkeeping — and collect the lines for this frame's writes.
|
|
542
|
-
* Pure accounting + the write list; the BYTES emit in the frame.
|
|
604
|
+
* Pure accounting + the write list; the BYTES emit in the frame.
|
|
605
|
+
* W11: the formula's blank above the cell (when one belongs) rides
|
|
606
|
+
* this cell's commit — the cache stays raw, the placed rows count. */
|
|
543
607
|
#commitCell(i, W, ctx) {
|
|
544
608
|
const cell = this.#cells[i];
|
|
545
609
|
const lines = cellComponent(cell).render(W, ctx);
|
|
546
610
|
this.#lineCache[i] = lines;
|
|
611
|
+
const placed = bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
547
612
|
this.#committed += 1;
|
|
548
|
-
this.#committedLines +=
|
|
549
|
-
this.#committedLinesThisFrame.push(...
|
|
613
|
+
this.#committedLines += placed.length;
|
|
614
|
+
this.#committedLinesThisFrame.push(...placed);
|
|
550
615
|
}
|
|
551
616
|
/** The slot occupant's extra rows — the slash-command menu (above the
|
|
552
617
|
* status, in the rhythm gap + the content's spare rows — the old
|
|
@@ -633,20 +698,24 @@ export class Body {
|
|
|
633
698
|
const committed = this.#committedLinesThisFrame;
|
|
634
699
|
// 0. the FROZEN rows — the re-folded committed content (re-flowed
|
|
635
700
|
// at the new width by the terminal): re-painted at [1..frozen],
|
|
636
|
-
// so the reflow's shifted copies can never ghost.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
701
|
+
// so the reflow's shifted copies can never ghost. W11: the
|
|
702
|
+
// formula's blanks between the cells are re-inserted here — the
|
|
703
|
+
// cache holds each cell's OWN rows; a missing blank would paint
|
|
704
|
+
// every row below it one row too high (the V6-1 idempotence
|
|
705
|
+
// rule: this draw must reproduce the screen, row by row).
|
|
706
|
+
// The bound is the CELL count at the frame's start — the force
|
|
707
|
+
// commit's placed LINES can exceed the cell count, and the old
|
|
708
|
+
// lines-bound went negative, skipping every previously-committed
|
|
709
|
+
// cell (the V6-1 frozen-loop finding — the banner vanished).
|
|
710
|
+
const frozen = [];
|
|
711
|
+
for (let i = 0; i < this.#committedAtFrameStart; i += 1) {
|
|
712
|
+
frozen.push(...bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, this.#lineCache[i]));
|
|
647
713
|
}
|
|
648
|
-
|
|
649
|
-
|
|
714
|
+
let r = 1;
|
|
715
|
+
// one march — the frozen, the committed (this frame's), the live —
|
|
716
|
+
// in write order, r monotone (three byte-identical loops merged for
|
|
717
|
+
// the gate; the exact write sequence preserved)
|
|
718
|
+
for (const line of [...frozen, ...committed, ...liveLines]) {
|
|
650
719
|
out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
|
|
651
720
|
r += 1;
|
|
652
721
|
}
|
|
@@ -664,7 +733,7 @@ export class Body {
|
|
|
664
733
|
const editor = this.#inputRow(W, ctx);
|
|
665
734
|
out.push(`\x1b[${H - 2};1H\x1b[0K${this.#checked(editor.stripped, W)}`);
|
|
666
735
|
out.push(`\x1b[${H - 1};1H\x1b[0K${footerLine(W)}`);
|
|
667
|
-
out.push(`\x1b[${H};1H\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`);
|
|
736
|
+
out.push(`\x1b[${H};1H\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W, this.#statusHint ?? undefined), W)}`);
|
|
668
737
|
// the cursor: up two (the input row at H−2) + left to the marker
|
|
669
738
|
out.push("\x1b[2A");
|
|
670
739
|
if (editor.afterW > 0)
|
|
@@ -689,7 +758,7 @@ export class Body {
|
|
|
689
758
|
out.push("\n");
|
|
690
759
|
// the bottom-up repaint, from the last row up — V6-3: the design
|
|
691
760
|
// §03 chrome: status (H), lower ╌ (H−1), input (H−2), upper ╌ (H−3)
|
|
692
|
-
out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`); // H — the status
|
|
761
|
+
out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W, this.#statusHint ?? undefined), W)}`); // H — the status
|
|
693
762
|
out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−1 — the lower ╌
|
|
694
763
|
out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(editor.stripped, W)}`); // H−2 — the input
|
|
695
764
|
out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−3 — the upper ╌
|
|
@@ -806,8 +875,8 @@ export class Dock {
|
|
|
806
875
|
onResize() {
|
|
807
876
|
compositorRef?.onResize();
|
|
808
877
|
}
|
|
809
|
-
setStatus(text) {
|
|
810
|
-
compositorRef?.setStatus(text);
|
|
878
|
+
setStatus(text, hint) {
|
|
879
|
+
compositorRef?.setStatus(text, hint ?? null);
|
|
811
880
|
}
|
|
812
881
|
setTail(tail) {
|
|
813
882
|
compositorRef?.setTail(tail);
|
package/dist/editor.d.ts
CHANGED
|
@@ -16,12 +16,8 @@
|
|
|
16
16
|
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
17
|
* inserts, internal newlines become spaces.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
/** Display width of a code-point array (cursor math, scrolling). */
|
|
22
|
-
export declare function widthOf(chars: readonly number[]): number;
|
|
23
|
-
/** Display width of a string. */
|
|
24
|
-
export declare function displayWidth(text: string): number;
|
|
19
|
+
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
20
|
+
export { charWidth, displayWidth, widthOf };
|
|
25
21
|
export declare const PROMPT = "\u258C ";
|
|
26
22
|
export declare const PROMPT_WIDTH: number;
|
|
27
23
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
package/dist/editor.js
CHANGED
|
@@ -16,56 +16,10 @@
|
|
|
16
16
|
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
17
|
* inserts, internal newlines become spaces.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (cp >= 0x2e80 && cp <= 0x303e)
|
|
24
|
-
return 2; // radicals .. CJK punctuation
|
|
25
|
-
if (cp >= 0x3041 && cp <= 0x33ff)
|
|
26
|
-
return 2; // kana, CJK compat
|
|
27
|
-
if (cp >= 0x3400 && cp <= 0x4dbf)
|
|
28
|
-
return 2; // CJK ext A
|
|
29
|
-
if (cp >= 0x4e00 && cp <= 0x9fff)
|
|
30
|
-
return 2; // CJK unified
|
|
31
|
-
if (cp >= 0xa000 && cp <= 0xa4cf)
|
|
32
|
-
return 2; // yi
|
|
33
|
-
if (cp >= 0xa960 && cp <= 0xa97f)
|
|
34
|
-
return 2; // hangul jamo ext
|
|
35
|
-
if (cp >= 0xac00 && cp <= 0xd7a3)
|
|
36
|
-
return 2; // hangul syllables
|
|
37
|
-
if (cp >= 0xf900 && cp <= 0xfaff)
|
|
38
|
-
return 2; // CJK compat ideographs
|
|
39
|
-
if (cp >= 0xfe10 && cp <= 0xfe19)
|
|
40
|
-
return 2; // vertical forms
|
|
41
|
-
if (cp >= 0xfe30 && cp <= 0xfe6f)
|
|
42
|
-
return 2; // CJK compat forms
|
|
43
|
-
if (cp >= 0xff00 && cp <= 0xff60)
|
|
44
|
-
return 2; // fullwidth forms
|
|
45
|
-
if (cp >= 0xffe0 && cp <= 0xffe6)
|
|
46
|
-
return 2; // fullwidth signs
|
|
47
|
-
if (cp >= 0x1f300 && cp <= 0x1f64f)
|
|
48
|
-
return 2; // emoji (misc + emoticons)
|
|
49
|
-
if (cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
50
|
-
return 2; // supplemental emoji
|
|
51
|
-
if (cp >= 0x20000 && cp <= 0x3fffd)
|
|
52
|
-
return 2; // CJK ext B..G
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
|
-
/** Display width of a code-point array (cursor math, scrolling). */
|
|
56
|
-
export function widthOf(chars) {
|
|
57
|
-
let w = 0;
|
|
58
|
-
for (const cp of chars)
|
|
59
|
-
w += charWidth(cp);
|
|
60
|
-
return w;
|
|
61
|
-
}
|
|
62
|
-
/** Display width of a string. */
|
|
63
|
-
export function displayWidth(text) {
|
|
64
|
-
let w = 0;
|
|
65
|
-
for (const ch of text)
|
|
66
|
-
w += charWidth(ch.codePointAt(0));
|
|
67
|
-
return w;
|
|
68
|
-
}
|
|
19
|
+
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
20
|
+
// the width primitives moved to width.ts (W1, the single width
|
|
21
|
+
// authority) — re-exported so the editor's public surface is unchanged.
|
|
22
|
+
export { charWidth, displayWidth, widthOf };
|
|
69
23
|
import { palette } from "./render.js";
|
|
70
24
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
71
25
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
@@ -98,7 +52,10 @@ export class Editor {
|
|
|
98
52
|
#pendingLines = []; // submits before onLine is wired (startup) — never dropped
|
|
99
53
|
#sigintCb = null;
|
|
100
54
|
#eotCb = null;
|
|
101
|
-
|
|
55
|
+
// W18: the escape LIST — the run-abort (chat) and the /compact cancel
|
|
56
|
+
// (dispatch) coexist; a listener removes itself via an unarmed guard
|
|
57
|
+
// (the compact's handler no-ops after its abort has fired).
|
|
58
|
+
#escapeCbs = [];
|
|
102
59
|
#onRender;
|
|
103
60
|
#menuOpen = false; // v3 §04: the slash-command menu
|
|
104
61
|
#menuSel = 0;
|
|
@@ -136,7 +93,7 @@ export class Editor {
|
|
|
136
93
|
this.#eotCb = cb;
|
|
137
94
|
}
|
|
138
95
|
onEscape(cb) {
|
|
139
|
-
this.#
|
|
96
|
+
this.#escapeCbs.push(cb);
|
|
140
97
|
}
|
|
141
98
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
142
99
|
line() {
|
|
@@ -253,7 +210,8 @@ export class Editor {
|
|
|
253
210
|
this.#onRender();
|
|
254
211
|
}
|
|
255
212
|
else {
|
|
256
|
-
this.#
|
|
213
|
+
for (const cb of [...this.#escapeCbs])
|
|
214
|
+
cb();
|
|
257
215
|
i += 1;
|
|
258
216
|
}
|
|
259
217
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
|
|
9
9
|
export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
10
10
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, type Palette, type PathResolver, type RecapStats, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
11
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
12
12
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
package/dist/index.js
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
|
|
9
9
|
export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
|
|
10
10
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, } from "./render.js";
|
|
11
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, } from "./render.js";
|
|
12
12
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|
package/dist/render.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface Palette {
|
|
|
25
25
|
readonly red: string;
|
|
26
26
|
readonly green: string;
|
|
27
27
|
readonly code: string;
|
|
28
|
+
readonly rv: string;
|
|
29
|
+
readonly rvEnd: string;
|
|
28
30
|
readonly reset: string;
|
|
29
31
|
}
|
|
30
32
|
export declare const COLOR_ON: Palette;
|
|
@@ -212,11 +214,24 @@ export declare function renderTerminalGap(statusLine: string | null): string;
|
|
|
212
214
|
* Pure.
|
|
213
215
|
*/
|
|
214
216
|
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
215
|
-
/** v3 §01: truncate a row at `width`, marking the hidden span
|
|
217
|
+
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
218
|
+
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
219
|
+
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
220
|
+
* marker's own cells are part of the row — the visible cut leaves room
|
|
221
|
+
* for it, so a truncated row never exceeds W (a cut row carries the
|
|
222
|
+
* marker INSIDE the width; a row that fits is returned untouched). */
|
|
216
223
|
export declare function truncateRow(row: string, width: number): string;
|
|
217
|
-
/** v3 §01 (V6-2): the banner lines for a width W
|
|
218
|
-
*
|
|
219
|
-
|
|
224
|
+
/** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
|
|
225
|
+
* the tier table (extends the existing "under 40 columns, skip the
|
|
226
|
+
* logo" rule with a HEIGHT input):
|
|
227
|
+
* W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
|
|
228
|
+
* W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
|
|
229
|
+
* anything smaller → text rows only
|
|
230
|
+
* then the blank, then "vX — tagline" — the art IS the wordmark, so the
|
|
231
|
+
* text row does not repeat the name — then extensions — then the W5
|
|
232
|
+
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
233
|
+
* with a " (+N)" marker. Pure. */
|
|
234
|
+
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
|
|
220
235
|
/** v3 §02 — the recap line that ends a run, replacing the "done" label +
|
|
221
236
|
* the old status line. All fields derive LOCALLY from the event stream
|
|
222
237
|
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
@@ -237,3 +252,19 @@ export declare function renderSessionLine(meta: {
|
|
|
237
252
|
runs: number;
|
|
238
253
|
updatedAt: number;
|
|
239
254
|
}): string;
|
|
255
|
+
/** W5 — the opening-screen resume list. Every field already exists
|
|
256
|
+
* behind renderSessionLine / `kiso sessions`: the relative time, the
|
|
257
|
+
* title, then the right-aligned "N events · M runs". The columns are
|
|
258
|
+
* fixed per W: 4 indent + 7 when + 1 + the title (the ONLY flexible
|
|
259
|
+
* field — cut with the ellipsis marker INSIDE the width) + 1 + the meta
|
|
260
|
+
* (padStart to metaW). The done-when: the meta's right edge lands at
|
|
261
|
+
* exactly W on every row. Returns PLAIN rows — the banner's uniform dim
|
|
262
|
+
* wrap styles them (no dim+bold SGR composition). */
|
|
263
|
+
export interface ResumeMeta {
|
|
264
|
+
readonly title: string;
|
|
265
|
+
readonly events: number;
|
|
266
|
+
readonly runs: number;
|
|
267
|
+
readonly updatedAt: number;
|
|
268
|
+
}
|
|
269
|
+
export declare function relativeTime(updatedAt: number, now: number): string;
|
|
270
|
+
export declare function renderResumeList(metas: readonly ResumeMeta[], W: number, now: number): string[];
|
package/dist/render.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
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
|
*/
|
|
10
|
-
|
|
11
|
-
export const
|
|
10
|
+
import { charWidth, displayWidth } from "./width.js";
|
|
11
|
+
export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;110m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
|
|
12
|
+
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
|
|
12
13
|
export function palette() {
|
|
13
14
|
return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
|
|
14
15
|
}
|
|
@@ -293,43 +294,81 @@ export function renderTerminalGap(statusLine) {
|
|
|
293
294
|
* Pure.
|
|
294
295
|
*/
|
|
295
296
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
297
|
+
/** v6's existing logo — W1's COMPACT tier, byte-identical, no redraw. */
|
|
296
298
|
const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
/** W1's BIG tier (36x6, `█` and space only — no half-blocks, so there is
|
|
300
|
+
* no tile seam to lose in a font that renders ▀ ▄ at the wrong height).
|
|
301
|
+
* Each pixel is two cells wide on purpose (a terminal cell is ~1:2);
|
|
302
|
+
* the render indents two — 38 columns total, clears 40. */
|
|
303
|
+
const BIG_LOGO_ROWS = [
|
|
304
|
+
"██ ██ ██████ ████████ ████████",
|
|
305
|
+
"██ ██ ██ ██ ██ ██",
|
|
306
|
+
"████ ██ ████████ ██ ██",
|
|
307
|
+
"████ ██ ██ ██ ██",
|
|
308
|
+
"██ ██ ██ ██ ██ ██",
|
|
309
|
+
"██ ██ ██████ ████████ ████████",
|
|
310
|
+
];
|
|
311
|
+
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
312
|
+
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
313
|
+
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
314
|
+
* marker's own cells are part of the row — the visible cut leaves room
|
|
315
|
+
* for it, so a truncated row never exceeds W (a cut row carries the
|
|
316
|
+
* marker INSIDE the width; a row that fits is returned untouched). */
|
|
307
317
|
export function truncateRow(row, width) {
|
|
308
|
-
|
|
318
|
+
const total = displayWidth(row);
|
|
319
|
+
if (total <= width)
|
|
309
320
|
return row;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
let
|
|
313
|
-
for (
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
// iterate the marker to a fixpoint: the marker's width changes the
|
|
322
|
+
// cut, the cut changes the hidden count the marker reports
|
|
323
|
+
let marker = " (+0)";
|
|
324
|
+
for (;;) {
|
|
325
|
+
const cut = Math.max(0, width - displayWidth(marker));
|
|
326
|
+
let w = 0;
|
|
327
|
+
let i = 0;
|
|
328
|
+
while (i < row.length) {
|
|
329
|
+
const cp = row.codePointAt(i);
|
|
330
|
+
const cw = charWidth(cp);
|
|
331
|
+
if (w + cw > cut)
|
|
332
|
+
break;
|
|
333
|
+
w += cw;
|
|
334
|
+
i += cp > 0xffff ? 2 : 1; // code-point stepping — never split a pair
|
|
335
|
+
}
|
|
336
|
+
const next = ` (+${total - w})`;
|
|
337
|
+
if (next === marker)
|
|
338
|
+
return `${row.slice(0, i)}${next}`;
|
|
339
|
+
marker = next;
|
|
318
340
|
}
|
|
319
|
-
return `${row.slice(0, i)} (+${displayW(row) - w})`;
|
|
320
341
|
}
|
|
321
|
-
/** v3 §01 (V6-2): the banner lines for a width W
|
|
322
|
-
*
|
|
323
|
-
|
|
342
|
+
/** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
|
|
343
|
+
* the tier table (extends the existing "under 40 columns, skip the
|
|
344
|
+
* logo" rule with a HEIGHT input):
|
|
345
|
+
* W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
|
|
346
|
+
* W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
|
|
347
|
+
* anything smaller → text rows only
|
|
348
|
+
* then the blank, then "vX — tagline" — the art IS the wordmark, so the
|
|
349
|
+
* text row does not repeat the name — then extensions — then the W5
|
|
350
|
+
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
351
|
+
* with a " (+N)" marker. Pure. */
|
|
352
|
+
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
|
|
324
353
|
const rows = [];
|
|
325
354
|
if (W >= 40) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
355
|
+
if (H >= 20) {
|
|
356
|
+
for (const r of BIG_LOGO_ROWS)
|
|
357
|
+
rows.push(truncateRow(` ${r}`, W));
|
|
358
|
+
}
|
|
359
|
+
else if (H >= 14) {
|
|
360
|
+
for (const r of LOGO_ROWS)
|
|
361
|
+
rows.push(truncateRow(r, W));
|
|
362
|
+
}
|
|
329
363
|
}
|
|
330
|
-
rows.
|
|
364
|
+
if (rows.length > 0)
|
|
365
|
+
rows.push("");
|
|
366
|
+
rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
|
|
331
367
|
if (extensionsText !== "")
|
|
332
368
|
rows.push(truncateRow(extensionsText, W));
|
|
369
|
+
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
370
|
+
rows.push("", ...renderResumeList(resume, W, now));
|
|
371
|
+
}
|
|
333
372
|
return rows;
|
|
334
373
|
}
|
|
335
374
|
export function renderRecap(s) {
|
|
@@ -353,3 +392,50 @@ export function renderSessionLine(meta) {
|
|
|
353
392
|
// round 8: the title is the user's first prompt — model/user text, escaped.
|
|
354
393
|
return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
|
|
355
394
|
}
|
|
395
|
+
export function relativeTime(updatedAt, now) {
|
|
396
|
+
const s = Math.max(0, now - updatedAt) / 1000;
|
|
397
|
+
if (s < 60)
|
|
398
|
+
return "now";
|
|
399
|
+
const m = Math.floor(s / 60);
|
|
400
|
+
if (m < 60)
|
|
401
|
+
return `${m}m ago`;
|
|
402
|
+
const h = Math.floor(m / 60);
|
|
403
|
+
if (h < 24)
|
|
404
|
+
return `${h}h ago`;
|
|
405
|
+
const d = Math.floor(h / 24);
|
|
406
|
+
if (d < 7)
|
|
407
|
+
return `${d}d ago`;
|
|
408
|
+
return `${Math.floor(d / 7)}w ago`;
|
|
409
|
+
}
|
|
410
|
+
function titleCut(text, max) {
|
|
411
|
+
if (displayWidth(text) <= max)
|
|
412
|
+
return text;
|
|
413
|
+
const room = max - displayWidth("…");
|
|
414
|
+
let w = 0;
|
|
415
|
+
let i = 0;
|
|
416
|
+
while (i < text.length) {
|
|
417
|
+
const cp = text.codePointAt(i);
|
|
418
|
+
const cw = charWidth(cp);
|
|
419
|
+
if (w + cw > room)
|
|
420
|
+
break;
|
|
421
|
+
w += cw;
|
|
422
|
+
i += cp > 0xffff ? 2 : 1;
|
|
423
|
+
}
|
|
424
|
+
return text.slice(0, i) + "…";
|
|
425
|
+
}
|
|
426
|
+
export function renderResumeList(metas, W, now) {
|
|
427
|
+
if (metas.length === 0)
|
|
428
|
+
return [];
|
|
429
|
+
const rows = [" ▞ resume"];
|
|
430
|
+
const whens = metas.map((m) => relativeTime(m.updatedAt, now));
|
|
431
|
+
const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
|
|
432
|
+
const metaW = Math.max(...metaTexts.map((t) => t.length));
|
|
433
|
+
const titleW = Math.max(1, W - 13 - metaW);
|
|
434
|
+
for (let i = 0; i < metas.length; i += 1) {
|
|
435
|
+
const title = escapeTerminal(metas[i].title);
|
|
436
|
+
const shown = titleCut(title, titleW);
|
|
437
|
+
const pad = titleW - displayWidth(shown);
|
|
438
|
+
rows.push(` ${whens[i].padEnd(7)} ${shown}${" ".repeat(pad)} ${metaTexts[i].padStart(metaW)}`);
|
|
439
|
+
}
|
|
440
|
+
return rows;
|
|
441
|
+
}
|
package/dist/width.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display-width primitives — the SINGLE width authority (TUI v5
|
|
3
|
+
* #16e: "charWidth is the width authority"). The eastAsianWidth table
|
|
4
|
+
* is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
|
|
5
|
+
* wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
|
|
6
|
+
* █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
|
|
7
|
+
* ZWJ clusters are not guaranteed perfect — each code point counts as
|
|
8
|
+
* its width. Zero dependencies (importable from any module).
|
|
9
|
+
*/
|
|
10
|
+
/** A code point's display width: 2 for the wide ranges, 1 otherwise. */
|
|
11
|
+
export declare function charWidth(cp: number): number;
|
|
12
|
+
/** Display width of a code-point array (cursor math, scrolling). */
|
|
13
|
+
export declare function widthOf(chars: readonly number[]): number;
|
|
14
|
+
/** Display width of a string. */
|
|
15
|
+
export declare function displayWidth(text: string): number;
|