@vincemakes/kiso-tui 0.1.32 → 0.1.34

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.
@@ -165,7 +165,10 @@ class UserMessage {
165
165
  }
166
166
  }
167
167
  /** The thinking fold — one dim line, width-capped so the /think suffix
168
- * rides the fold's own row (the #17 fix's slice, componentized). */
168
+ * rides the fold's own row (the #17 fix's slice, componentized). The
169
+ * slice is DISPLAY-WIDTH-based (the char-based slice overflowed with
170
+ * CJK — 2 cells per char — and tripped invariant ① on a real
171
+ * Chinese session). */
169
172
  class ThinkingFold {
170
173
  cell;
171
174
  constructor(cell) {
@@ -178,7 +181,7 @@ class ThinkingFold {
178
181
  return [`${palette().dim}…${trimmed}${palette().reset}`];
179
182
  const suffix = ` (${block.length} chars · /think)`;
180
183
  const slice = Math.max(1, W - 1 - suffix.length);
181
- return [`${palette().dim}…${trimmed.slice(0, slice)}${suffix}${palette().reset}`];
184
+ return [`${palette().dim}…${widthCut(trimmed, slice)}${suffix}${palette().reset}`];
182
185
  }
183
186
  }
184
187
  /** The tool execution line + the approval mini-diff — every state is
@@ -14,10 +14,10 @@
14
14
  * re-emitted — the native scrollback gets them, reflow-safe, and
15
15
  * the user's shell history is never touched (zero \x1b[3J, zero
16
16
  * replay);
17
- * - the live region (content + status + editor + footer + menu) is
18
- * hard-capped at H−1 lines; overflow FORCE-commits the oldest live
19
- * line regardless of done-ness — the one sharp edge this round
20
- * introduces (asserted by the VT-emulator gate);
17
+ * - the live region (content + chrome + menu) is hard-capped: the
18
+ * content at H−4 (V6-3 — the four-row chrome); overflow FORCE-
19
+ * commits the oldest live line regardless of done-ness — the one
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
23
  * tui-main-screen.ts:447-473, no silent truncate); ② within the
@@ -32,9 +32,10 @@
32
32
  * scheduler — a one-shot setTimeout re-armed only while a running
33
33
  * tool exists (the #14/#15 zero-output contract is structural).
34
34
  *
35
- * Layout at H rows: content rows 1..H−3, status H−2, editor (the slot)
36
- * H−1, footerat H. Pipes / NO_COLOR: the passthrough branches below
37
- * keep the v2a/v2b line-mode bytes byte-for-byte (the e2e guards them).
35
+ * Layout at H rows (V6-3 the design §03 chrome): content rows
36
+ * 1..H−4, upper ╌ H−3, editor (the slot) H−2, lower H−1, status H.
37
+ * Pipes / NO_COLOR: the passthrough branches below keep the v2a/v2b
38
+ * line-mode bytes byte-for-byte (the e2e guards them).
38
39
  */
39
40
  import { type MenuItem } from "./editor.js";
40
41
  /** The cursor marker — an APC private sequence the focus component
@@ -14,10 +14,10 @@
14
14
  * re-emitted — the native scrollback gets them, reflow-safe, and
15
15
  * the user's shell history is never touched (zero \x1b[3J, zero
16
16
  * replay);
17
- * - the live region (content + status + editor + footer + menu) is
18
- * hard-capped at H−1 lines; overflow FORCE-commits the oldest live
19
- * line regardless of done-ness — the one sharp edge this round
20
- * introduces (asserted by the VT-emulator gate);
17
+ * - the live region (content + chrome + menu) is hard-capped: the
18
+ * content at H−4 (V6-3 — the four-row chrome); overflow FORCE-
19
+ * commits the oldest live line regardless of done-ness — the one
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
23
  * tui-main-screen.ts:447-473, no silent truncate); ② within the
@@ -32,9 +32,10 @@
32
32
  * scheduler — a one-shot setTimeout re-armed only while a running
33
33
  * tool exists (the #14/#15 zero-output contract is structural).
34
34
  *
35
- * Layout at H rows: content rows 1..H−3, status H−2, editor (the slot)
36
- * H−1, footerat H. Pipes / NO_COLOR: the passthrough branches below
37
- * keep the v2a/v2b line-mode bytes byte-for-byte (the e2e guards them).
35
+ * Layout at H rows (V6-3 the design §03 chrome): content rows
36
+ * 1..H−4, upper ╌ H−3, editor (the slot) H−2, lower H−1, status H.
37
+ * Pipes / NO_COLOR: the passthrough branches below keep the v2a/v2b
38
+ * line-mode bytes byte-for-byte (the e2e guards them).
38
39
  */
39
40
  import { truncateDiff } from "./diff.js";
40
41
  import { displayWidth } from "./editor.js";
@@ -46,7 +47,7 @@ import { escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, r
46
47
  export const CURSOR_MARKER = "\x1b_[kiso-cur]\x1b\\";
47
48
  const FRAME_MS = 16; // state changes coalesce to ≥16ms frames
48
49
  const SPINNER_MS = 200; // the spinner cadence — a ONE-SHOT re-armed on demand
49
- const CHROME_ROWS = 3; // status + editor + footer — the cap's constant part
50
+ const CHROME_ROWS = 4; // upper + input + lower ╌ + status — the design §03 chrome (V6-3)
50
51
  /** The one compositor — implements the Body façade AND the Dock chrome
51
52
  * API (see the class comments on each method group). */
52
53
  export class Body {
@@ -332,7 +333,7 @@ export class Body {
332
333
  const H = this.#lastH > 0 ? this.#lastH : process.stdout.rows ?? 24;
333
334
  const out = [];
334
335
  out.push("\x1b[r");
335
- for (let row = H - 2; row <= H; row += 1) {
336
+ for (let row = H - 3; row <= H; row += 1) { // V6-3: the four chrome rows
336
337
  out.push(`\x1b[${row};1H\x1b[0K`); // clear the three chrome rows
337
338
  }
338
339
  out.push(`\x1b[${Math.max(1, H - 1)};1H`);
@@ -470,6 +471,21 @@ export class Body {
470
471
  return;
471
472
  this.#lastH = H;
472
473
  const ctx = { spinnerI: this.#spinnerI, now: Date.now() };
474
+ // V6-1 (the screen-state == frame-state rule): the resize's first
475
+ // frame — the terminal's reflow re-wrapped the committed content at
476
+ // the NEW width, so the cached folds are stale. Re-fold the
477
+ // committed cells so the every-row draw below re-paints them at the
478
+ // current geometry — the frame's model and the screen agree.
479
+ if (this.#fullRedraw) {
480
+ this.#lineCache = this.#lineCache.map(() => null);
481
+ this.#committedLines = 0;
482
+ for (let i = 0; i < this.#committed; i += 1) {
483
+ const cell = this.#cells[i];
484
+ const lines = cellComponent(cell).render(W, ctx);
485
+ this.#lineCache[i] = lines;
486
+ this.#committedLines += lines.length;
487
+ }
488
+ }
473
489
  // 1. the natural commits — the leading DONE cells freeze: their
474
490
  // lines leave the live region, the scrolls + the committed
475
491
  // writes below place them (the #17 "freeze as a real line",
@@ -490,7 +506,7 @@ export class Body {
490
506
  // 3. the FORCE commits — the live region's hard cap H−1: overflow
491
507
  // commits the oldest live cell UNCONDITIONALLY (the one sharp
492
508
  // edge — the cap scalar is asserted by the gates).
493
- while (liveLines.length + chromeRows > H - 1 && this.#committed < this.#cells.length) {
509
+ while (liveLines.length > H - 4 && this.#committed < this.#cells.length) { // V6-3: the content cap H−4
494
510
  this.#commitCell(this.#committed, W, ctx);
495
511
  liveLines = [];
496
512
  for (const cell of this.#cells.slice(this.#committed)) {
@@ -601,28 +617,53 @@ export class Body {
601
617
  }
602
618
  /** The full-redraw path (the first frame, the resize repaint) — CUP
603
619
  * allowed here; zero LF; zero \x1b[3J; zero replay. The committed
604
- * lines (this frame's) write at [liveTop−N .. liveTop−1]. */
620
+ * lines (this frame's) write at [liveTop−N .. liveTop−1].
621
+ *
622
+ * V6-1 (the screen-state == frame-state rule): every row 1..H is
623
+ * covered — the committed/live/chrome writes AND the EL-only rows
624
+ * (above the committed section, the gap). The terminal's reflow
625
+ * re-wraps the old content at the new size — its shifted copies
626
+ * survive anywhere the draw does not touch; a draw that covers
627
+ * EVERY row is idempotent: N consecutive resizes end with the same
628
+ * screen as a single jump to the same size. */
605
629
  #drawFull(out, W, H, liveTop, liveLines, menuRows, ctx) {
606
630
  const committed = this.#committedLinesThisFrame;
607
- let r = Math.max(1, liveTop - committed.length);
631
+ // 0. the FROZEN rows — the re-folded committed content (re-flowed
632
+ // 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).
608
641
  for (const line of committed) {
609
642
  out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
610
643
  r += 1;
611
644
  }
645
+ // 2. the live lines.
612
646
  for (const line of liveLines) {
613
647
  out.push(`\x1b[${r};1H\x1b[0K${this.#checked(line, W)}`);
614
648
  r += 1;
615
649
  }
616
- const menuTop = H - 2 - menuRows.length;
650
+ // 3. the GAP rows (between the live content and the chrome) — EL.
651
+ for (let rr = r; rr <= H - 4; rr += 1) {
652
+ out.push(`\x1b[${rr};1H\x1b[0K`);
653
+ }
654
+ const menuTop = H - 3 - menuRows.length;
617
655
  for (let i = 0; i < menuRows.length; i += 1) {
618
656
  out.push(`\x1b[${menuTop + i};1H\x1b[0K${this.#checked(menuRows[i], W)}`);
619
657
  }
620
- out.push(`\x1b[${H - 2};1H\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`);
658
+ // V6-3: the design §03 chrome upper ╌ (H−3), input (H−2),
659
+ // lower ╌ (H−1), status (H).
660
+ out.push(`\x1b[${H - 3};1H\x1b[0K${footerLine(W)}`);
621
661
  const editor = this.#inputRow(W, ctx);
622
- out.push(`\x1b[${H - 1};1H\x1b[0K${this.#checked(editor.stripped, W)}`);
623
- out.push(`\x1b[${H};1H\x1b[0K${footerLine(W)}`);
624
- // the cursor: up one (the editor row) + left to the marker column
625
- out.push("\x1b[1A");
662
+ out.push(`\x1b[${H - 2};1H\x1b[0K${this.#checked(editor.stripped, W)}`);
663
+ 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)}`);
665
+ // the cursor: up two (the input row at H−2) + left to the marker
666
+ out.push("\x1b[2A");
626
667
  if (editor.afterW > 0)
627
668
  out.push(`\x1b[${editor.afterW}D`);
628
669
  }
@@ -640,12 +681,16 @@ export class Body {
640
681
  out.push("\n");
641
682
  }
642
683
  else {
643
- out.push("\x1b[1B"); // to the footer row
644
- }
645
- // the bottom-up repaint, from the last row up
646
- out.push(`\x1b[1G\x1b[0K${footerLine(W)}`); // H
647
- out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(editor.stripped, W)}`); // H−1 — the editor
648
- out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`); // H−2
684
+ // no commits — jump from the anchor (H−2) straight to the
685
+ // bottom row H (the commit path's LFs left the cursor there)
686
+ out.push("\x1b[2B");
687
+ }
688
+ // the bottom-up repaint, from the last row up V6-3: the design
689
+ // §03 chrome: status (H), lower (H−1), input (H−2), upper (H−3)
690
+ out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`); // H — the status
691
+ out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−1 — the lower ╌
692
+ out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(editor.stripped, W)}`); // H−2 — the input
693
+ out.push(`\x1b[1A\x1b[1G\x1b[0K${footerLine(W)}`); // H−3 — the upper ╌
649
694
  for (let i = menuRows.length - 1; i >= 0; i -= 1) {
650
695
  out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(menuRows[i], W)}`);
651
696
  }
@@ -659,7 +704,7 @@ export class Body {
659
704
  // CRLF) cannot misplace them the way a relative march could).
660
705
  // 1. the GAP rows (between the live content and the chrome) — EL'd
661
706
  // so the old content there cannot ghost.
662
- for (let r = liveTop + liveLines.length; r <= H - 3; r += 1) {
707
+ for (let r = liveTop + liveLines.length; r <= H - 4; r += 1) {
663
708
  out.push(`\x1b[${r};1H\x1b[0K`);
664
709
  }
665
710
  // 2. the STALE rows above the committed section — the scrolled old
@@ -675,11 +720,11 @@ export class Body {
675
720
  for (let i = 0; i < committed.length; i += 1) {
676
721
  out.push(`\x1b[${Math.max(1, liveTop - committed.length + i)};1H\x1b[0K${this.#checked(committed[i], W)}`);
677
722
  }
678
- // the cursor: down to the anchor (H−1) + left to the marker column
723
+ // the cursor: down to the anchor (H−2, the input row) + left to the marker —
679
724
  // the down-distance from the LAST written row (the committed bottom,
680
725
  // the stale bottom, or the gap bottom when nothing else wrote).
681
- const lastRow = committed.length > 0 ? liveTop - 1 : staleFrom < liveTop ? liveTop - 1 : H - 3;
682
- const down = H - 1 - lastRow;
726
+ const lastRow = committed.length > 0 ? liveTop - 1 : staleFrom < liveTop ? liveTop - 1 : H - 4;
727
+ const down = H - 2 - lastRow; // the anchor: the input row (H−2)
683
728
  if (down > 0)
684
729
  out.push(`\x1b[${down}B`);
685
730
  if (editor.afterW > 0)
package/dist/render.d.ts CHANGED
@@ -202,17 +202,20 @@ export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio
202
202
  */
203
203
  export declare function renderTerminalGap(statusLine: string | null): string;
204
204
  /**
205
- * v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
206
- * (TOP / the tagline / BOTTOM), then TWO info rows (version,
207
- * extensions). Every row truncates at the terminal width with a " (+N)"
208
- * marker (N = the hidden display width); a window narrower than 40
209
- * columns skips the logo entirely only the info rows. Pure.
205
+ * v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
206
+ * (the logo.svg pixel form K I S O), then a BLANK, then the info rows:
207
+ * "kiso vX tagline" + extensions. The tagline rides the version line
208
+ * (the old logo MIDDLE row was the tagline a text row masquerading as
209
+ * the logo's centre). Every row truncates at the terminal width with a
210
+ * " (+N)" marker (N = the hidden display width); a window narrower than
211
+ * 40 columns skips the logo + the blank entirely — only the info rows.
212
+ * Pure.
210
213
  */
211
214
  export declare const TAGLINE = "the coding agent that survives kill -9";
212
215
  /** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
213
216
  export declare function truncateRow(row: string, width: number): string;
214
- /** v3 §01: the banner lines for a width W — logo (skipped under 40
215
- * columns) + version + extensions. */
217
+ /** v3 §01 (V6-2): the banner lines for a width W — logo (skipped under
218
+ * 40 columns) + blank + "kiso vX — tagline" + extensions. */
216
219
  export declare function bannerLines(W: number, version: string, extensionsText: string): string[];
217
220
  /** v3 §02 — the recap line that ends a run, replacing the "done" label +
218
221
  * the old status line. All fields derive LOCALLY from the event stream
package/dist/render.js CHANGED
@@ -283,14 +283,17 @@ export function renderTerminalGap(statusLine) {
283
283
  return `${statusLine === null ? "" : `${statusLine}\n`}\n`;
284
284
  }
285
285
  /**
286
- * v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
287
- * (TOP / the tagline / BOTTOM), then TWO info rows (version,
288
- * extensions). Every row truncates at the terminal width with a " (+N)"
289
- * marker (N = the hidden display width); a window narrower than 40
290
- * columns skips the logo entirely only the info rows. Pure.
286
+ * v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
287
+ * (the logo.svg pixel form K I S O), then a BLANK, then the info rows:
288
+ * "kiso vX tagline" + extensions. The tagline rides the version line
289
+ * (the old logo MIDDLE row was the tagline a text row masquerading as
290
+ * the logo's centre). Every row truncates at the terminal width with a
291
+ * " (+N)" marker (N = the hidden display width); a window narrower than
292
+ * 40 columns skips the logo + the blank entirely — only the info rows.
293
+ * Pure.
291
294
  */
292
295
  export const TAGLINE = "the coding agent that survives kill -9";
293
- const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", TAGLINE, "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
296
+ const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
294
297
  /** Display width of a row (1-cell ASCII; 2-cell CJK/wide). */
295
298
  function displayW(row) {
296
299
  let w = 0;
@@ -315,14 +318,16 @@ export function truncateRow(row, width) {
315
318
  }
316
319
  return `${row.slice(0, i)} (+${displayW(row) - w})`;
317
320
  }
318
- /** v3 §01: the banner lines for a width W — logo (skipped under 40
319
- * columns) + version + extensions. */
321
+ /** v3 §01 (V6-2): the banner lines for a width W — logo (skipped under
322
+ * 40 columns) + blank + "kiso vX — tagline" + extensions. */
320
323
  export function bannerLines(W, version, extensionsText) {
321
324
  const rows = [];
322
- if (W >= 40)
325
+ if (W >= 40) {
323
326
  for (const r of LOGO_ROWS)
324
327
  rows.push(truncateRow(r, W));
325
- rows.push(truncateRow(`v${version}`, W));
328
+ rows.push("");
329
+ }
330
+ rows.push(truncateRow(`kiso v${version} — ${TAGLINE}`, W));
326
331
  if (extensionsText !== "")
327
332
  rows.push(truncateRow(extensionsText, W));
328
333
  return rows;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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",