@vincemakes/kiso-tui 0.10.0 → 0.11.0

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.
Files changed (2) hide show
  1. package/dist/compositor.js +90 -17
  2. package/package.json +2 -2
@@ -51,7 +51,7 @@ import { panelAffordanceOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./as
51
51
  import { atPanelRows, bandHeader } from "./at-picker.js";
52
52
  // TUI2-R2 ②: the session picker's rows — the band's third occupant.
53
53
  import { sessionPickerRows } from "./session-picker.js";
54
- import { Container, ROLLUP_NOUN, SPINNER, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
54
+ import { Container, ROLLUP_NOUN, SPINNER, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
55
55
  import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
56
56
  import { displayVerb, keysSheetRows } from "./strings.js";
57
57
  /** The cursor marker — an APC private sequence the focus component
@@ -96,6 +96,11 @@ export class Body {
96
96
  #lastTool = null;
97
97
  #pendingCalls = new Map();
98
98
  #pipeBuf = ""; // the passthrough's thinking buffer
99
+ /** TUI2-MD ⑤ — the markdown scanner of the message currently
100
+ * streaming, and the cell index its first block landed at. Null
101
+ * between messages: the scanner's life is one assistant message. */
102
+ #md = null;
103
+ #mdBase = 0;
99
104
  #toolCells = new Map(); // callId → cell index (parallel tools)
100
105
  // W15: the collapsed (cut) tool cells — committed cells whose last
101
106
  // rendered row carried the "ctrl+r" affordance; the expand key's
@@ -398,22 +403,71 @@ export class Body {
398
403
  const turn = this.#turns[this.#turns.length - 1];
399
404
  if (turn !== undefined)
400
405
  turn.hasText = true;
401
- const last = this.#cells[this.#cells.length - 1];
402
- if (last !== undefined && last.kind === "text" && !last.done) {
403
- last.text += text;
404
- }
405
- else {
406
+ // TUI2-MD ⑤: assistant body text is MARKDOWN, scanned as it
407
+ // streams. The scanner yields CLOSED blocks (final source, final
408
+ // render) and one OPEN tail block; each becomes a cell, and the
409
+ // cell is the commit unit the compositor already had — so
410
+ // block-freeze needs no new commit machinery at all. A closed
411
+ // block is a DONE cell the natural loop freezes; the tail is the
412
+ // one cell left live, repainting in place.
413
+ if (this.#md === null) {
406
414
  this.#closeOpenThinking();
407
415
  this.#closeOpenText();
408
- this.#cells.push({ kind: "text", text, done: false });
416
+ this.#md = new MdStream();
417
+ this.#mdBase = this.#cells.length;
409
418
  }
419
+ this.#md.push(text);
420
+ this.#syncMd();
410
421
  this.#mark();
411
422
  }
423
+ /** TUI2-MD ⑤ — mirror the scanner's blocks onto cells. Append-only by
424
+ * construction: a block that has closed never changes, so a cell that
425
+ * is done is never touched again (and a committed one could not be).
426
+ * Only the trailing tail cell is rewritten per delta. */
427
+ #syncMd() {
428
+ if (this.#md === null)
429
+ return;
430
+ const blocks = this.#md.blocks();
431
+ const closed = this.#md.closed();
432
+ for (let i = 0; i < blocks.length; i += 1) {
433
+ const at = this.#mdBase + i;
434
+ const cell = this.#cells[at];
435
+ if (cell === undefined) {
436
+ this.#cells.push({ kind: "md", block: blocks[i], done: i < closed });
437
+ continue;
438
+ }
439
+ if (cell.kind !== "md" || cell.done)
440
+ continue;
441
+ cell.block = blocks[i];
442
+ cell.done = i < closed;
443
+ }
444
+ // the tail can vanish (a lone whitespace delta that turns out to be
445
+ // a blank line). Drop it only where dropping is safe: never below
446
+ // the commit frontier, where the bytes are already the terminal's.
447
+ const want = this.#mdBase + blocks.length;
448
+ while (this.#cells.length > Math.max(want, this.#committed) && this.#cells[this.#cells.length - 1].kind === "md")
449
+ this.#cells.pop();
450
+ }
451
+ /** TUI2-MD ⑤ — the message ends: the tail block closes and every md
452
+ * cell of this message is final. */
453
+ #endMd() {
454
+ if (this.#md === null)
455
+ return;
456
+ this.#md.end();
457
+ this.#syncMd();
458
+ for (let i = this.#mdBase; i < this.#cells.length; i += 1) {
459
+ const cell = this.#cells[i];
460
+ if (cell.kind === "md")
461
+ cell.done = true;
462
+ }
463
+ this.#md = null;
464
+ }
412
465
  textEnd() {
413
466
  if (!this.#isActive()) {
414
467
  this.#write("\n");
415
468
  return;
416
469
  }
470
+ this.#endMd();
417
471
  const last = this.#cells[this.#cells.length - 1];
418
472
  if (last !== undefined && last.kind === "text" && !last.done)
419
473
  last.done = true;
@@ -923,14 +977,13 @@ export class Body {
923
977
  inputExtra +
924
978
  queueRows.length);
925
979
  }
926
- const live = this.#cells.slice(this.#committed);
927
980
  const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
928
981
  const W = this.#opts.width();
929
982
  let lines = 0;
930
983
  let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
931
- for (const cell of live) {
932
- const rows = cellComponent(cell).render(W, ctx);
933
- lines += bodySpacing(prev, rows).length;
984
+ for (let i = this.#committed; i < this.#cells.length; i += 1) {
985
+ const rows = cellComponent(this.#cells[i]).render(W, ctx);
986
+ lines += this.#space(i, prev, rows).length;
934
987
  prev = rows;
935
988
  }
936
989
  return lines + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
@@ -963,7 +1016,7 @@ export class Body {
963
1016
  const cell = this.#cells[i];
964
1017
  const lines = cellComponent(cell).render(W, ctx);
965
1018
  this.#lineCache[i] = lines; // the cell's OWN rows — the cache stays raw
966
- this.#committedLines += bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines).length;
1019
+ this.#committedLines += this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines).length;
967
1020
  }
968
1021
  }
969
1022
  // 1. the natural commits — the leading DONE cells freeze: their
@@ -1042,7 +1095,7 @@ export class Body {
1042
1095
  // nowhere else, which is what makes "exactly one" structural
1043
1096
  if (i === focus && rows.length > 0)
1044
1097
  rows[0] = focusToken(rows[0], W);
1045
- liveLines.push(...bodySpacing(prev, rows));
1098
+ liveLines.push(...this.#space(i, prev, rows));
1046
1099
  prev = rows;
1047
1100
  }
1048
1101
  }
@@ -1063,7 +1116,7 @@ export class Body {
1063
1116
  const rows = cellComponent(cell).render(W, ctx);
1064
1117
  if (i === focus && rows.length > 0)
1065
1118
  rows[0] = focusToken(rows[0], W);
1066
- liveLines.push(...bodySpacing(prev, rows));
1119
+ liveLines.push(...this.#space(i, prev, rows));
1067
1120
  prev = rows;
1068
1121
  }
1069
1122
  }
@@ -1106,6 +1159,23 @@ export class Body {
1106
1159
  // composer (N = 1 ⇒ H−2, the retired hard-coded anchor).
1107
1160
  this.#lastAnchorRow = H - 1 - editor.rows.length + editor.markerRow;
1108
1161
  }
1162
+ /** TUI2-MD ⑤ — the join blank between cell i−1 and cell i.
1163
+ *
1164
+ * W11's formula ("a blank above a row that is itself a block, or
1165
+ * whose previous sibling was taller than one row") reads ROW COUNTS,
1166
+ * and markdown's rhythm is not a row count: a heading wants a blank
1167
+ * above and below it even between two one-row paragraphs, and two
1168
+ * rows of one fence want none even when a long code line folds to
1169
+ * two. So between two MARKDOWN cells the formula steps aside and the
1170
+ * block's own `gap` decides — the renderer owns the rhythm, which is
1171
+ * the only place that knows it. Every other pair is untouched,
1172
+ * including the boundary INTO a markdown message (the blank under the
1173
+ * user chip is still W11's). */
1174
+ #space(i, prev, rows) {
1175
+ if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
1176
+ return rows;
1177
+ return bodySpacing(prev, rows);
1178
+ }
1109
1179
  /** Commit the cell at index i: render + cache its lines (immutable —
1110
1180
  * the force-committed form freezes at the current render), advance
1111
1181
  * the bookkeeping — and collect the lines for this frame's writes.
@@ -1128,7 +1198,7 @@ export class Body {
1128
1198
  if (cell.kind === "tool" && lines.some((l) => l.includes("ctrl+r")))
1129
1199
  this.#collapsed.unshift(i);
1130
1200
  this.#lineCache[i] = lines;
1131
- const placed = bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, lines);
1201
+ const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
1132
1202
  this.#committed += 1;
1133
1203
  this.#committedLines += placed.length;
1134
1204
  this.#committedLinesThisFrame.push(...placed);
@@ -1544,7 +1614,7 @@ export class Body {
1544
1614
  // cell (the V6-1 frozen-loop finding — the banner vanished).
1545
1615
  const frozen = [];
1546
1616
  for (let i = 0; i < this.#committedAtFrameStart; i += 1) {
1547
- frozen.push(...bodySpacing(i > 0 ? this.#lineCache[i - 1] : null, this.#lineCache[i]));
1617
+ frozen.push(...this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, this.#lineCache[i]));
1548
1618
  }
1549
1619
  // A8: the march is the WINDOW — the model's last H rows. When the
1550
1620
  // model total (committed + live + chrome) exceeds H, the window's
@@ -1884,8 +1954,11 @@ export class Body {
1884
1954
  return i === undefined ? null : (this.#cells[i] ?? null);
1885
1955
  }
1886
1956
  /** Close an open TEXT cell when a new cell starts (see v2d — the
1887
- * runtime emits no text_end; the next cell is the close signal). */
1957
+ * runtime emits no text_end; the next cell is the close signal).
1958
+ * TUI2-MD ⑤: that same signal ends the markdown message — the open
1959
+ * tail block closes and its cell becomes commit-eligible. */
1888
1960
  #closeOpenText() {
1961
+ this.#endMd();
1889
1962
  const last = this.#cells[this.#cells.length - 1];
1890
1963
  if (last !== undefined && last.kind === "text" && !last.done)
1891
1964
  last.done = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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",
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
37
  "dependencies": {
38
- "@vincemakes/kiso-tui-cells": "0.10.0"
38
+ "@vincemakes/kiso-tui-cells": "0.11.0"
39
39
  }
40
40
  }