@vincemakes/kiso-tui 0.24.4 → 0.25.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.
@@ -227,31 +227,6 @@ export declare class Body {
227
227
  * there is no pointer, so there is nothing a pointer could reach
228
228
  * that a keyboard cannot. */
229
229
  viewerKey(cmd: "up" | "down" | "toggle" | "all" | "pageUp" | "pageDown" | "home" | "end"): void;
230
- /**
231
- * DC-35 — ctrl+o does not print the same expansion twice in a row.
232
- *
233
- * The ring walks newest-back and restarts its cycle once every entry
234
- * has been opened (R4/C1, which is what makes the walk immune to the
235
- * ring growing underneath it). With a ring of ONE the restart is
236
- * immediate, so holding the key appended the identical block over
237
- * and over — the owner got three copies of the same four rows, each
238
- * closing with `ctrl+o opens the one before it`, a footer naming
239
- * something that does not exist.
240
- *
241
- * The bar is the BOTTOM of the transcript, not "ever shown": once
242
- * other content has arrived the expansion has scrolled up and
243
- * re-opening it is the point of the key, so the guard clears itself
244
- * the moment a cell is added.
245
- */
246
- expandNext(): {
247
- kind: "toggled";
248
- } | {
249
- kind: "appended";
250
- lines: string[];
251
- } | {
252
- kind: "none";
253
- why?: "already-last";
254
- };
255
230
  /** Docked = the chrome is live (a color TTY with a real size). */
256
231
  get active(): boolean;
257
232
  enter(): void;
@@ -313,6 +288,24 @@ export declare class Body {
313
288
  * repaint, once per session.
314
289
  */
315
290
  onGroundChange(): void;
291
+ /**
292
+ * DC-50 — ctrl+o flips ONE switch, and every settled card obeys it.
293
+ *
294
+ * The walk this replaces existed because ADR-0046 §3 forbade
295
+ * re-rendering a committed card, so the only way to show one's body
296
+ * was to APPEND a copy further down — with a ring to decide which
297
+ * card was next, `#opened` to keep the ring from repeating, and
298
+ * `#lastAppend` to stop a held key printing the same rows three times
299
+ * (DC-35). Amendment 1 removes the premise: the scrollback is ours to
300
+ * erase, so a card can simply be re-rendered where it stands.
301
+ *
302
+ * A RUNNING card is exempt. Its height is E2/DC-43's — it grows from
303
+ * its own content — and a global "show everything" has no business
304
+ * reaching into a card whose content is still arriving.
305
+ */
306
+ toggleExpanded(): void;
307
+ /** DC-50 — the switch itself, for the CLI's affordance text. */
308
+ expandedAll(): boolean;
316
309
  /** W18: the status row's right-aligned hint is part of the status
317
310
  * state — the compacting row passes "esc to cancel" (the affordance
318
311
  * must survive repaints). */
@@ -52,7 +52,7 @@ import { MOUSE_OFF } from "./editor.js";
52
52
  import { atPanelRows, bandHeader } from "./at-picker.js";
53
53
  // TUI2-R2 ②: the session picker's rows — the band's third occupant.
54
54
  import { sessionPickerRows } from "./session-picker.js";
55
- import { CAP_PREVIEW, thinkingRow, expandedCard, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, foldCountsObjects, foldTerms, focusToken, foldLine, gutterCut, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, visibleWidth, breathFrame, } from "./components.js";
55
+ import { CAP_PREVIEW, thinkingRow, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, foldCountsObjects, foldTerms, focusToken, foldLine, gutterCut, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, visibleWidth, breathFrame, } from "./components.js";
56
56
  import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
57
57
  import { displayVerb, keysSheetRows } from "./strings.js";
58
58
  // R5 — the transcript viewer's PURE projection. The compositor supplies
@@ -212,6 +212,30 @@ export class Body {
212
212
  /** DC-34 — the previous frame's width; the reach-back guard is for a
213
213
  * WIDTH change, which re-indexes the model, not a height change. */
214
214
  #lastW = 0;
215
+ /** R14 — the geometry the last SETTLE ran at. A winch back to the
216
+ * same size is not a resize and must not erase the scrollback. */
217
+ /** R14 — the geometry the last frame PAINTED, and the snapshot of it
218
+ * taken when a winch opens a settle window.
219
+ *
220
+ * Three versions of this question were wrong before this one.
221
+ * Seeding in `enter()` never ran under the unit pool (it bails on a
222
+ * non-TTY), so a same-size winch erased the scrollback. Comparing
223
+ * against the CURRENT frame's geometry let an ordinary frame render
224
+ * at the new size between the winch and the settle and eat the
225
+ * change, so a real resize silently stopped reprinting. Seeding once
226
+ * on the first frame had the same hole whenever the first frame had
227
+ * not run yet.
228
+ *
229
+ * The question the settle actually asks is "is the terminal holding
230
+ * a rendering made at a DIFFERENT geometry than the one we have
231
+ * now" — so the comparison is against what was on screen when the
232
+ * winch arrived, snapshotted then, once per storm. */
233
+ #paintedW = 0;
234
+ #paintedH = 0;
235
+ #winchFromW = 0;
236
+ #winchFromH = 0;
237
+ /** DC-50 — the ONE expansion switch ctrl+o flips. */
238
+ #expandedAll = false;
215
239
  // KC1 §6: the composer's recorded extent — the row count the last
216
240
  // frame drew (exit's clear walks it) and the row its CHA parked the
217
241
  // cursor on (the steady frame's relative anchor). N = 1 reproduces
@@ -237,18 +261,12 @@ export class Body {
237
261
  #mdBase = 0;
238
262
  #toolCells = new Map(); // callId → cell index (parallel tools)
239
263
  // W15: the collapsed (cut) tool cells — committed cells whose last
240
- // rendered row carried the "ctrl+o" affordance; the expand key's
241
- // cycling pointer walks this list from the newest back.
264
+ // rendered row carried the "ctrl+o" affordance.
265
+ //
266
+ // DC-50 / R14: the expand key no longer WALKS this. It is the
267
+ // VIEWER's index now (ctrl+r, `#viewerEntries`) — the two surfaces
268
+ // coexist and §9 says so. It is kept, not retired, for that reader.
242
269
  #collapsed = [];
243
- /** R4 (C1) — the ring walk is by IDENTITY, not by a modular pointer.
244
- * `#collapsed` is unshifted on every commit that carries the key, so
245
- * a numeric pointer's target silently CHANGED whenever a new fold
246
- * landed mid-cycle: the ring was not stable under itself, and the
247
- * next press opened something other than what the last press
248
- * implied. This set records what the current cycle has already
249
- * opened; the walk takes the newest entry not in it, and empties it
250
- * when every entry has been seen. */
251
- #opened = new Set();
252
270
  // W14: the turn records — one per userLine, the fold-hold's state
253
271
  // machine (ended / hasText / folded) plus the folded-turn line's
254
272
  // counts (accumulated at toolStart). The cells carry the record's
@@ -1167,112 +1185,32 @@ export class Body {
1167
1185
  cutLine(`${p.dim} ${viewerHint(this.#viewer, entries)}${p.reset}`, W),
1168
1186
  ];
1169
1187
  }
1170
- /** DC-35 — the last block this key appended, and the cell count when
1171
- * it did. An expansion earns its rows by showing something the
1172
- * transcript does not already END with. */
1173
- #lastAppend = null;
1174
- /**
1175
- * DC-35 ctrl+o does not print the same expansion twice in a row.
1176
- *
1177
- * The ring walks newest-back and restarts its cycle once every entry
1178
- * has been opened (R4/C1, which is what makes the walk immune to the
1179
- * ring growing underneath it). With a ring of ONE the restart is
1180
- * immediate, so holding the key appended the identical block over
1181
- * and over — the owner got three copies of the same four rows, each
1182
- * closing with `ctrl+o opens the one before it`, a footer naming
1183
- * something that does not exist.
1184
- *
1185
- * The bar is the BOTTOM of the transcript, not "ever shown": once
1186
- * other content has arrived the expansion has scrolled up and
1187
- * re-opening it is the point of the key, so the guard clears itself
1188
- * the moment a cell is added.
1188
+ /*
1189
+ * RETIRED (DC-50 / R14, 2026-09-05) `expandNext`, `#expandNextRaw`,
1190
+ * `#lastAppend`, and the `#opened` bookkeeping that went with them.
1191
+ *
1192
+ * The whole apparatus served ONE constraint: ADR-0046 §3 forbade
1193
+ * re-rendering a committed card, so the only way to show one's body
1194
+ * was to APPEND a copy further down the transcript. That needed a ring
1195
+ * to choose which card came next, a set to stop the ring repeating
1196
+ * itself, and DC-35's guard to stop a HELD key printing the same four
1197
+ * rows three times a defect the owner met in the field, each copy
1198
+ * closing with a footer that named something which did not exist.
1199
+ *
1200
+ * Amendment 1 removes the constraint. The terminal's scrollback is
1201
+ * ours to erase, so a committed card is re-rendered where it stands:
1202
+ * nothing to append, nothing to choose between, no repeat to guard.
1203
+ * `toggleExpanded` is the whole of the feature now.
1204
+ *
1205
+ * `#collapsed` STAYS the viewer (ctrl+r) reads it as its index of
1206
+ * expandable cells, and §9 keeps the two surfaces side by side.
1207
+ *
1208
+ * `expandedCard` does NOT stay, and an earlier draft of this note said
1209
+ * it did. An expanded card is drawn by the ORDINARY card renderer with
1210
+ * `expanded` set — full body, `ctrl+o collapses` in place of the cut
1211
+ * note — which `r14-global-expand` pins. `expandedCard` was the shape
1212
+ * of the APPENDED block and has no caller once the append is gone.
1189
1213
  */
1190
- expandNext() {
1191
- const out = this.#expandNextRaw();
1192
- if (out.kind !== "appended")
1193
- return out;
1194
- const lines = out.lines.join("\n");
1195
- if (this.#lastAppend !== null && this.#lastAppend.lines === lines && this.#lastAppend.atCells === this.#cells.length) {
1196
- // NOT the same answer as "nothing is folded". The caller says
1197
- // which, because a reader who pressed the key deserves to know
1198
- // whether there is nothing to open or whether they are already
1199
- // looking at it.
1200
- return { kind: "none", why: "already-last" };
1201
- }
1202
- this.#lastAppend = { lines, atCells: this.#cells.length };
1203
- return out;
1204
- }
1205
- #expandNextRaw() {
1206
- for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
1207
- const cell = this.#cells[i];
1208
- if (cell.kind === "tool" && cell.state !== "pending") {
1209
- cell.expanded = !cell.expanded;
1210
- this.#mark();
1211
- return { kind: "toggled" };
1212
- }
1213
- // W20: the LIVE task block toggles in place too — the capped
1214
- // form flips to the full list (the "done-collapse expands
1215
- // under ctrl+o" claim). The SETTLED block is already full —
1216
- // no toggle, and its rows carry no affordance, so it never
1217
- // joins #collapsed (the committed /last append is moot).
1218
- if (cell.kind === "checklist" && !cell.done) {
1219
- cell.expanded = !cell.expanded;
1220
- this.#mark();
1221
- return { kind: "toggled" };
1222
- }
1223
- }
1224
- if (this.#collapsed.length === 0)
1225
- return { kind: "none" };
1226
- // R4 (C1) — the newest entry this cycle has not opened yet. When
1227
- // every entry has been seen the cycle restarts, so the walk is
1228
- // still "newest back" — it is simply immune to the ring growing
1229
- // underneath it.
1230
- if (this.#collapsed.every((i) => this.#opened.has(i)))
1231
- this.#opened.clear();
1232
- const idx = this.#collapsed.find((i) => !this.#opened.has(i)) ?? this.#collapsed[0];
1233
- this.#opened.add(idx);
1234
- const cell = this.#cells[idx];
1235
- // R3b — a folded SEGMENT expands to the work it stands for.
1236
- //
1237
- // The fold line collapses a run of thinking and tool cells into
1238
- // one row; without this the run would be unreachable, which is
1239
- // hiding a durable record behind a summary. The rows are APPENDED
1240
- // (ADR-0046 — history is never rewritten), exactly as every other
1241
- // expand in this method does, and they are the cells' OWN renders,
1242
- // so the expansion cannot drift from what was folded.
1243
- // R13 — expandNext's FOLD branch retired with the segment fold.
1244
- // A fold line collapsed a run of cells into one row, so the key had
1245
- // to be able to open the run; with every call standing as its own
1246
- // card there is nothing collapsed for it to open, and `#collapsed`
1247
- // now holds cards alone.
1248
- if (cell.kind !== "tool")
1249
- return { kind: "none" };
1250
- // R13 — and the ROLLUP branch retired with `rolled`.
1251
- let input = {};
1252
- try {
1253
- input = JSON.parse(cell.inputFull);
1254
- }
1255
- catch {
1256
- // the full JSON is always parseable (it was stringified at
1257
- // toolStart) — the empty fallback never fires
1258
- }
1259
- const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
1260
- const p = palette();
1261
- // 0.24.2 ③ — the expansion is a CARD, and `✦` is not its mark: that
1262
- // glyph is the turn recap's, and one symbol with two meanings is
1263
- // §4.1. The head row names the call, which is the tie to it that
1264
- // the mark's sentence used to be.
1265
- const verb = displayVerb(cell.name);
1266
- const elapsed = cell.startedAt !== null && cell.doneAt !== null ? ((cell.doneAt - cell.startedAt) / 1000).toFixed(1) : "?";
1267
- const n = cell.resultText === "" ? 0 : cell.resultText.split("\n").length;
1268
- return {
1269
- kind: "appended",
1270
- lines: expandedCard(verb, toolTarget(cell.name, input), `expanded · ${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`,
1271
- // TUI2-R2pre ④: the SECTION HEADERS say the act; the payloads
1272
- // below them (inputFull, resultText) are RAW and byte-identical.
1273
- [`--- ${verb} input ---`, cell.inputFull, `--- ${verb} output${cell.isError ? " (error)" : ""} ---`, cell.resultText], [cell.isError ? "failed" : "exit 0", `${n} line${n === 1 ? "" : "s"}`, `${elapsed}s`].filter((x) => x !== "").join(" · "), this.#opts.width()),
1274
- };
1275
- }
1276
1214
  // ---- the Dock façade (the CLI's chrome API — same shape as the old dock) ----
1277
1215
  /** Docked = the chrome is live (a color TTY with a real size). */
1278
1216
  get active() {
@@ -1506,6 +1444,14 @@ export class Body {
1506
1444
  onResize() {
1507
1445
  if (!this.#isActive())
1508
1446
  return;
1447
+ // R14 — the FIRST winch of a storm snapshots what the terminal is
1448
+ // currently holding. A drag fires many winches; the settle must
1449
+ // compare its end against the storm's beginning, not against the
1450
+ // previous winch.
1451
+ if (!this.#resizePending) {
1452
+ this.#winchFromW = this.#paintedW;
1453
+ this.#winchFromH = this.#paintedH;
1454
+ }
1509
1455
  this.#resizePending = true;
1510
1456
  if (this.#resizeTimer !== null)
1511
1457
  clearTimeout(this.#resizeTimer);
@@ -1533,20 +1479,128 @@ export class Body {
1533
1479
  this.#screen = [];
1534
1480
  this.onResize();
1535
1481
  }
1536
- /** The one repaint a drag earns, once its signals have stopped. */
1482
+ /**
1483
+ * R14 / route B — THE ONE REPAINT A DRAG EARNS IS A REPRINT.
1484
+ *
1485
+ * It used to be `ESC[from;1H ESC[0J` — erase from the recorded live
1486
+ * top down, then redraw the live area, leaving the committed rows
1487
+ * above to whatever the terminal's own reflow had made of them. That
1488
+ * is the shape R10 measured on the owner's real terminal, and it
1489
+ * lost: a grow dropped 16 rows of history (DC-39), a narrow
1490
+ * duplicated four tokens, and the scrolled-off transcript never
1491
+ * reflowed at all. All three are one fault — kiso doing window
1492
+ * arithmetic over rows the terminal had already reflowed underneath
1493
+ * it, with `#scrolledOff` carried across a fold change that makes
1494
+ * every index mean something else.
1495
+ *
1496
+ * So it stops arguing. `2J H 3J` — erase screen, home, erase
1497
+ * scrollback, in THAT order because on Apple Terminal a bare `2J`
1498
+ * scrolls the screen into history and only the following `3J` makes
1499
+ * the state clean — and then the whole committed transcript is
1500
+ * reprinted at the new geometry through the path a fresh terminal
1501
+ * already uses. The terminal ends holding exactly one rendering.
1502
+ *
1503
+ * The declared cost (ADR-0046 Amendment 1): everything the terminal
1504
+ * held before kiso started is erased at the first resize. Measured
1505
+ * on the reference implementation as 0/60 in every direction — this
1506
+ * is the behaviour being adopted, not a regression. kiso's own
1507
+ * record is untouched: the session log holds it, `--resume` replays
1508
+ * it, and G4' asserts the 0/60 so the cost cannot drift silently.
1509
+ *
1510
+ * D-B2: EVERY settled resize reprints, height-only included. A
1511
+ * SIGWINCH at the same geometry emits nothing at all.
1512
+ */
1537
1513
  #settleResize() {
1538
1514
  if (!this.#resizePending || !this.#isActive())
1539
1515
  return;
1540
1516
  this.#resizePending = false;
1541
1517
  const H = this.#opts.height();
1542
- const liveRows = this.#lastLiveRows > 0 ? this.#lastLiveRows : 3;
1543
- const from = Math.max(1, (this.#lastH > 0 ? this.#lastH : H) - liveRows + 1);
1544
- this.#write(`\x1b[${Math.min(from, Math.max(1, H))};1H\x1b[0J`);
1518
+ const W = this.#opts.width();
1519
+ // D-B2's other half: a winch that did not change the geometry is
1520
+ // not a resize. Emitting the erase for it would throw away the
1521
+ // terminal's scrollback for nothing (the V6-1 idempotence case).
1522
+ if (this.#winchFromW === W && this.#winchFromH === H)
1523
+ return;
1524
+ // §9.2 — the viewer is closed first. Nothing commits while it is
1525
+ // up and a reprint is a commit storm; the user reopens it. One
1526
+ // keypress, stated rather than hidden.
1527
+ if (this.#viewer !== null) {
1528
+ this.#viewer = null;
1529
+ this.#viewerWasUp = true;
1530
+ }
1531
+ this.#reprint();
1532
+ }
1533
+ /**
1534
+ * R14 — ERASE THE TERMINAL AND PRINT THE SESSION AGAIN.
1535
+ *
1536
+ * Two callers: a settled resize, and DC-50's ctrl+o. They are the
1537
+ * same act — the rendering the terminal holds is wrong (wrong
1538
+ * geometry, or wrong expansion state) and the model is the only
1539
+ * authority on what it should be — so they share the path rather
1540
+ * than growing two.
1541
+ */
1542
+ #reprint() {
1543
+ const H = this.#opts.height();
1544
+ this.#write("\x1b[2J\x1b[H\x1b[3J");
1545
+ // The terminal now holds nothing, so every record of what it held
1546
+ // is void. `#scrolledOff` is the frontier of what reached its
1547
+ // scrollback: after the erase, that is zero — which is also what
1548
+ // releases the committed cells to be refolded at the new width
1549
+ // without contradicting anything, the thing DC-34's frontier rule
1550
+ // existed to prevent.
1551
+ this.#scrolledOff = 0;
1552
+ for (let i = 0; i < this.#committed; i += 1)
1553
+ this.#lineCache[i] = null;
1554
+ this.#screen = new Array(Math.max(1, H)).fill(NOT_PAINTED);
1555
+ this.#cursorRow = 1;
1545
1556
  this.#fullRedraw = true;
1546
1557
  this.#resizeFrame = true;
1547
1558
  this.#dirty = true;
1548
1559
  this.render();
1549
1560
  }
1561
+ /**
1562
+ * DC-50 — ctrl+o flips ONE switch, and every settled card obeys it.
1563
+ *
1564
+ * The walk this replaces existed because ADR-0046 §3 forbade
1565
+ * re-rendering a committed card, so the only way to show one's body
1566
+ * was to APPEND a copy further down — with a ring to decide which
1567
+ * card was next, `#opened` to keep the ring from repeating, and
1568
+ * `#lastAppend` to stop a held key printing the same rows three times
1569
+ * (DC-35). Amendment 1 removes the premise: the scrollback is ours to
1570
+ * erase, so a card can simply be re-rendered where it stands.
1571
+ *
1572
+ * A RUNNING card is exempt. Its height is E2/DC-43's — it grows from
1573
+ * its own content — and a global "show everything" has no business
1574
+ * reaching into a card whose content is still arriving.
1575
+ */
1576
+ toggleExpanded() {
1577
+ this.#expandedAll = !this.#expandedAll;
1578
+ for (const cell of this.#cells) {
1579
+ // SETTLED CONTENT, not "done". A card parked for approval is
1580
+ // `state: "approval"`, `done: false` — and its content is not
1581
+ // still arriving: the diff is complete and the card is waiting
1582
+ // for a human to read it and decide. The mechanism this
1583
+ // replaces toggled it (any state but "pending"), and the
1584
+ // dispatch comment said why: "the approval pause is exactly
1585
+ // when the user reads a cut diff, and the key must answer then,
1586
+ // never after the run." A `done`-only switch answers with
1587
+ // nothing at the one moment the answer matters most.
1588
+ //
1589
+ // The exemption is for the card still GROWING — E2 and DC-43
1590
+ // own its height, and a global "show everything" has no
1591
+ // business reaching into content that is still arriving. That
1592
+ // is the ruling's reason; `done` alone was its wording.
1593
+ if (cell.kind !== "tool")
1594
+ continue;
1595
+ if (cell.done || cell.state === "approval")
1596
+ cell.expanded = this.#expandedAll;
1597
+ }
1598
+ this.#reprint();
1599
+ }
1600
+ /** DC-50 — the switch itself, for the CLI's affordance text. */
1601
+ expandedAll() {
1602
+ return this.#expandedAll;
1603
+ }
1550
1604
  /** W18: the status row's right-aligned hint is part of the status
1551
1605
  * state — the compacting row passes "esc to cancel" (the affordance
1552
1606
  * must survive repaints). */
@@ -1949,6 +2003,10 @@ export class Body {
1949
2003
  if (H < 4)
1950
2004
  return;
1951
2005
  this.#lastH = H;
2006
+ // R14 — the geometry THIS frame painted. `onResize` snapshots it
2007
+ // at the moment the winch arrives; see `#winchFromW`.
2008
+ this.#paintedW = W;
2009
+ this.#paintedH = H;
1952
2010
  const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
1953
2011
  // V6-1 (the screen-state == frame-state rule): the resize's first
1954
2012
  // frame — the terminal's reflow re-wrapped the committed content at
@@ -2384,11 +2442,9 @@ export class Body {
2384
2442
  const hidesRows = cell.kind === "tool" && lines.some((l) => l.includes("ctrl+o"));
2385
2443
  if (isFoldHead || hidesRows) {
2386
2444
  this.#collapsed.unshift(i);
2387
- // R4a a new fold resets the walk, so the FIRST press after any
2388
- // new work always opens the most recent one. That is the whole
2389
- // of the owner's "which one does it open": the answer is always
2390
- // "the last one", and repeats walk back from there.
2391
- this.#opened.clear();
2445
+ // R4a's walk reset retired with the walk (DC-50): there is no
2446
+ // "which one does it open" any more one press opens all of
2447
+ // them.
2392
2448
  }
2393
2449
  this.#lineCache[i] = lines;
2394
2450
  const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
@@ -2944,38 +3000,31 @@ export class Body {
2944
3000
  // sit with the live band empty and the chrome at its minimum — the
2945
3001
  // one-way part of a movement that otherwise goes both ways.
2946
3002
  if (this.#resizeFrame) {
2947
- // REL-0152-R1: a resize scrolls NOTHING of ours. Shrinking the
2948
- // window is the terminal's own scroll — it reflows the old
2949
- // content and pushes the overflow into its scrollback before we
2950
- // are called — so emitting our own LFs on top put the same rows
2951
- // in twice (TT-1B: twelve rows of a forty-line burst). The
2952
- // counter adopts the terminal's work, and the held screen is
2953
- // discarded because a reflow invalidates every row of it: the
2954
- // next diff repaints the whole screen, which is exactly what a
2955
- // resize needs.
2956
- // DC-34 — NO HIGH-WATER MARK ON A RESIZE.
3003
+ // R14 / route B THE RESIZE FRAME SCROLLS LIKE ANY OTHER.
2957
3004
  //
2958
- // This was `max(#scrolledOff, …)`, which held a stale count
2959
- // whenever a widen made the fresh one smaller; `leaving` then
2960
- // stayed <= 0 and the text that marched past in the meantime
2961
- // never entered the scrollback at all the hole, the other
2962
- // half of the same off-by-a-refold.
3005
+ // Everything this branch used to hold is retired with the
3006
+ // window arithmetic it served. It adopted the terminal's own
3007
+ // reflow-scroll into `#scrolledOff` (REL-0152-R1), then had to
3008
+ // stop adopting it on a widen and keep adopting it on a narrow
3009
+ // (DC-34), because the counter it was feeding meant a different
3010
+ // thing at every fold width. There is no such counter to feed
3011
+ // now: `#settleResize` erased the terminal and set
3012
+ // `#scrolledOff` to zero, so the frontier is real and empty and
3013
+ // the frame simply reprints from the top.
2963
3014
  //
2964
- // The other implementation in this space reached the same
2965
- // conclusion independently and says so in its own source: a
2966
- // historical high-water mark "caused self-reinforcing
2967
- // inflation that pushed content into scrollback on terminal
2968
- // widen". Dropping it alone brings the DUPLICATE back — it is
2969
- // the pair with the no-refold rule above, not a substitute
2970
- // for it.
2971
- // PROBE 3: a widen leaves it ALONE; a narrow keeps REL-0152-R1.
2972
- if (this.#refolded)
2973
- this.#scrolledOff = Math.max(this.#scrolledOff, Math.max(0, Math.min(skip, all.length)));
3015
+ // Falling through to `#emitScroll` is not a convenience — it is
3016
+ // the whole point. The transcript above the last screenful has
3017
+ // to reach the scrollback, chunked and staged from the model,
3018
+ // exactly as a resumed session's replay does. Without it the
3019
+ // reprint painted the last screenful onto a terminal whose
3020
+ // history it had just erased, and everything above was simply
3021
+ // GONE: `dc34-widen-seam` measured 36 tokens missing from the
3022
+ // scrollback the first time this branch was left in place.
2974
3023
  this.#refolded = false;
2975
3024
  this.#screen = new Array(H).fill(NOT_PAINTED);
2976
3025
  this.#resizeFrame = false;
2977
3026
  }
2978
- else if (!overlay) {
3027
+ if (!overlay) {
2979
3028
  // DC-46 — THE ROWS THAT LEAVE THE WINDOW SCROLL, and the target
2980
3029
  // is `skip` itself.
2981
3030
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.24.4",
3
+ "version": "0.25.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.24.4"
38
+ "@vincemakes/kiso-tui-cells": "0.25.0"
39
39
  }
40
40
  }