@vincemakes/kiso-tui 0.1.35 → 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.
@@ -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
@@ -38,6 +41,7 @@
38
41
  * line-mode bytes byte-for-byte (the e2e guards them).
39
42
  */
40
43
  import { type MenuItem } from "./editor.js";
44
+ import { type ResumeMeta } from "./render.js";
41
45
  /** The cursor marker — an APC private sequence the focus component
42
46
  * embeds at the edit position; the compositor strips it and moves
43
47
  * relatively (it never reaches the terminal). */
@@ -83,6 +87,14 @@ export declare class Body {
83
87
  text: string;
84
88
  status: "pending" | "active" | "done";
85
89
  }[]): void;
90
+ /** The startup banner (W1): a LIVE cell — the tier re-derives per
91
+ * frame (bannerLines with the CURRENT W and H), so a resize re-tiers
92
+ * the art instead of re-folding frozen rows (a window below 40 cols
93
+ * never paints the logo). W5: the resume metas ride the cell — the
94
+ * list re-gates with the tier (BIG only) and re-times with the
95
+ * frame. The inactive path keeps the historical bytes (no resume —
96
+ * the pipe contract). */
97
+ banner(version: string, extensionsText: string, resume?: ResumeMeta[]): void;
86
98
  raw(lines: string[]): void;
87
99
  /** The last COMPLETE thinking block, for /think. */
88
100
  lastThinking(): string | null;
@@ -109,7 +121,10 @@ export declare class Body {
109
121
  * top: external writes (the CLI's console.error CRLF) can shift the
110
122
  * committed content down, and a formula-top ED0 would clear it. */
111
123
  onResize(): void;
112
- setStatus(text: string): void;
124
+ /** W18: the status row's right-aligned hint is part of the status
125
+ * state — the compacting row passes "esc to cancel" (the affordance
126
+ * must survive repaints). */
127
+ setStatus(text: string, hint?: string | null): void;
113
128
  setTail(tail: string): void;
114
129
  showQuestion(question: string): void;
115
130
  clearQuestion(): void;
@@ -135,7 +150,9 @@ export declare class Body {
135
150
  /** Teardown — flush a pending frame, stop the timers. */
136
151
  close(): void;
137
152
  /** The live region's scalar — the unit tests assert the cap directly
138
- * (the e2e gate pins the screen consequence). */
153
+ * (the e2e gate pins the screen consequence). W11: the formula's
154
+ * blanks are join artifacts — the count includes them (they are real
155
+ * screen rows), threaded against the previous sibling's OWN rows. */
139
156
  liveCount(): number;
140
157
  render(): void;
141
158
  }
@@ -149,7 +166,7 @@ export declare class Dock {
149
166
  enter(): void;
150
167
  exit(): void;
151
168
  onResize(): void;
152
- setStatus(text: string): void;
169
+ setStatus(text: string, hint?: string | null): void;
153
170
  setTail(tail: string): void;
154
171
  showQuestion(question: string): void;
155
172
  clearQuestion(): void;
@@ -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
@@ -39,8 +42,8 @@
39
42
  */
40
43
  import { truncateDiff } from "./diff.js";
41
44
  import { displayWidth } from "./editor.js";
42
- import { Container, SPINNER, cellComponent, foldLine, footerLine, statusLine, visibleWidth, } from "./components.js";
43
- 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";
44
47
  /** The cursor marker — an APC private sequence the focus component
45
48
  * embeds at the edit position; the compositor strips it and moves
46
49
  * relatively (it never reaches the terminal). */
@@ -75,6 +78,7 @@ export class Body {
75
78
  #resizeHandler = null;
76
79
  // the chrome state (the Dock façade)
77
80
  #status = "";
81
+ #statusHint = null;
78
82
  #tail = "";
79
83
  #question = null;
80
84
  #inputState = () => ({ line: "", cursor: 0 });
@@ -151,8 +155,18 @@ export class Body {
151
155
  this.#write(`→ ${escapeTerminal(name)}(${escapeTerminal(JSON.stringify(input).slice(0, 200))})\n`);
152
156
  return;
153
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
+ }
154
168
  this.#toolCells.set(callId, this.#cells.length);
155
- 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 });
156
170
  this.#mark();
157
171
  }
158
172
  toolApproval(callId, diff) {
@@ -283,6 +297,30 @@ export class Body {
283
297
  this.#cells.push({ kind: "checklist", header, items, done: true });
284
298
  this.#mark();
285
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
+ }
286
324
  raw(lines) {
287
325
  if (!this.#isActive()) {
288
326
  this.#closeOpenThinking();
@@ -357,8 +395,12 @@ export class Body {
357
395
  this.#dirty = true;
358
396
  this.render(); // the immediate redraw at the NEW geometry
359
397
  }
360
- setStatus(text) {
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) {
361
402
  this.#status = text;
403
+ this.#statusHint = hint;
362
404
  this.redraw();
363
405
  }
364
406
  setTail(tail) {
@@ -449,19 +491,29 @@ export class Body {
449
491
  }
450
492
  // ---- the one writer ----
451
493
  /** The live region's scalar — the unit tests assert the cap directly
452
- * (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. */
453
497
  liveCount() {
454
498
  const live = this.#cells.slice(this.#committed);
455
- const ctx = { spinnerI: this.#spinnerI, now: Date.now() };
499
+ const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
456
500
  const W = this.#opts.width();
457
501
  let lines = 0;
458
- for (const cell of live)
459
- lines += cellComponent(cell).render(W, ctx).length;
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
+ }
460
508
  return lines + CHROME_ROWS + this.#menuRows(W).length;
461
509
  }
462
510
  /** The lines committed THIS frame — the writes land in the frame's
463
511
  * committed section (the rows just above the live region). */
464
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;
465
517
  render() {
466
518
  if (!this.#isActive())
467
519
  return;
@@ -470,7 +522,7 @@ export class Body {
470
522
  if (H < 4)
471
523
  return;
472
524
  this.#lastH = H;
473
- const ctx = { spinnerI: this.#spinnerI, now: Date.now() };
525
+ const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
474
526
  // V6-1 (the screen-state == frame-state rule): the resize's first
475
527
  // frame — the terminal's reflow re-wrapped the committed content at
476
528
  // the NEW width, so the cached folds are stale. Re-fold the
@@ -482,8 +534,8 @@ export class Body {
482
534
  for (let i = 0; i < this.#committed; i += 1) {
483
535
  const cell = this.#cells[i];
484
536
  const lines = cellComponent(cell).render(W, ctx);
485
- this.#lineCache[i] = lines;
486
- 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;
487
539
  }
488
540
  }
489
541
  // 1. the natural commits — the leading DONE cells freeze: their
@@ -492,16 +544,24 @@ export class Body {
492
544
  // short sessions included — the frame coalescing keeps a
493
545
  // cell's first frame its freeze frame, so the frozen bytes
494
546
  // emit exactly once).
547
+ this.#committedAtFrameStart = this.#committed;
495
548
  this.#committedLinesThisFrame = [];
496
549
  while (this.#committed < this.#cells.length && this.#cells[this.#committed].done) {
497
550
  this.#commitCell(this.#committed, W, ctx);
498
551
  }
499
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).
500
555
  const menuRows = this.#menuRows(W);
501
556
  const chromeRows = CHROME_ROWS + menuRows.length;
502
557
  let liveLines = [];
503
- for (const cell of this.#cells.slice(this.#committed)) {
504
- liveLines.push(...cellComponent(cell).render(W, ctx));
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
+ }
505
565
  }
506
566
  // 3. the FORCE commits — the live region's hard cap H−1: overflow
507
567
  // commits the oldest live cell UNCONDITIONALLY (the one sharp
@@ -509,8 +569,13 @@ export class Body {
509
569
  while (liveLines.length > H - 4 && this.#committed < this.#cells.length) { // V6-3: the content cap H−4
510
570
  this.#commitCell(this.#committed, W, ctx);
511
571
  liveLines = [];
512
- for (const cell of this.#cells.slice(this.#committed)) {
513
- liveLines.push(...cellComponent(cell).render(W, ctx));
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
+ }
514
579
  }
515
580
  }
516
581
  // 4. the geometry — the live region's first row:
@@ -536,14 +601,17 @@ export class Body {
536
601
  /** Commit the cell at index i: render + cache its lines (immutable —
537
602
  * the force-committed form freezes at the current render), advance
538
603
  * the bookkeeping — and collect the lines for this frame's writes.
539
- * 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. */
540
607
  #commitCell(i, W, ctx) {
541
608
  const cell = this.#cells[i];
542
609
  const lines = cellComponent(cell).render(W, ctx);
543
610
  this.#lineCache[i] = lines;
611
+ const placed = bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines);
544
612
  this.#committed += 1;
545
- this.#committedLines += lines.length;
546
- this.#committedLinesThisFrame.push(...lines);
613
+ this.#committedLines += placed.length;
614
+ this.#committedLinesThisFrame.push(...placed);
547
615
  }
548
616
  /** The slot occupant's extra rows — the slash-command menu (above the
549
617
  * status, in the rhythm gap + the content's spare rows — the old
@@ -630,20 +698,24 @@ export class Body {
630
698
  const committed = this.#committedLinesThisFrame;
631
699
  // 0. the FROZEN rows — the re-folded committed content (re-flowed
632
700
  // at the new width by the terminal): re-painted at [1..frozen],
633
- // so the reflow's shifted copies can never ghost.
634
- const frozen = this.#lineCache.slice(0, this.#committed - committed.length).flat().filter((l) => l !== null);
635
- let r = 1;
636
- for (const line of frozen) {
637
- out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
638
- r += 1;
639
- }
640
- // 1. the committed lines (this frame's).
641
- for (const line of committed) {
642
- out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
643
- r += 1;
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]));
644
713
  }
645
- // 2. the live lines.
646
- for (const line of liveLines) {
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]) {
647
719
  out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
648
720
  r += 1;
649
721
  }
@@ -661,7 +733,7 @@ export class Body {
661
733
  const editor = this.#inputRow(W, ctx);
662
734
  out.push(`\x1b[${H - 2};1H\x1b[0K${this.#checked(editor.stripped, W)}`);
663
735
  out.push(`\x1b[${H - 1};1H\x1b[0K${footerLine(W)}`);
664
- 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)}`);
665
737
  // the cursor: up two (the input row at H−2) + left to the marker
666
738
  out.push("\x1b[2A");
667
739
  if (editor.afterW > 0)
@@ -686,7 +758,7 @@ export class Body {
686
758
  out.push("\n");
687
759
  // the bottom-up repaint, from the last row up — V6-3: the design
688
760
  // §03 chrome: status (H), lower ╌ (H−1), input (H−2), upper ╌ (H−3)
689
- 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
690
762
  out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−1 — the lower ╌
691
763
  out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(editor.stripped, W)}`); // H−2 — the input
692
764
  out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−3 — the upper ╌
@@ -803,8 +875,8 @@ export class Dock {
803
875
  onResize() {
804
876
  compositorRef?.onResize();
805
877
  }
806
- setStatus(text) {
807
- compositorRef?.setStatus(text);
878
+ setStatus(text, hint) {
879
+ compositorRef?.setStatus(text, hint ?? null);
808
880
  }
809
881
  setTail(tail) {
810
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
- /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
20
- export declare function charWidth(cp: number): number;
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
- /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
20
- export function charWidth(cp) {
21
- if (cp >= 0x1100 && cp <= 0x115f)
22
- return 2; // hangul jamo
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
- #escapeCb = null;
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.#escapeCb = cb;
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.#escapeCb?.();
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 " (+N)". */
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 logo (skipped under
218
- * 40 columns) + blank + "kiso vX tagline" + extensions. */
219
- export declare function bannerLines(W: number, version: string, extensionsText: string): string[];
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[];