@vincemakes/kiso-tui 0.22.0 → 0.24.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.
@@ -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 { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, gutterCut, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth, breathFrame, } from "./components.js";
55
+ import { CAP_PREVIEW, 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
@@ -80,102 +80,8 @@ const LIVE_ACT_HEADS = 3;
80
80
  /** R8 — the command band's window: five rows plus a counter, the same
81
81
  * budget the composer's own ceiling can afford above it. */
82
82
  const MENU_WINDOW = 5;
83
- /** W13 / TUI2-R1 (B) a rolled run's TITLE: the exploration sentence on
84
- * a mixed run, W13's verb+count on a single-name one. */
85
- function rolledTitle(cell) {
86
- const r = cell.rolled;
87
- if (r.parts !== undefined)
88
- return `explored ${exploreCounts(r.parts)}`;
89
- return `${displayVerb(cell.name)} ${r.count} ${ROLLUP_NOUN[cell.name] ?? "calls"}`;
90
- }
91
- /** W13 / TUI2-R1 (B) — a rolled run's DETAIL rows: one row per tool with
92
- * its subjects on a mixed run, one `└ target` per call on a single-name
93
- * one.
94
- *
95
- * Extracted at R3b so the segment fold's expansion opens a run to the
96
- * SAME rows `ctrl+o` on the run itself would have opened. Two copies of
97
- * this would be two answers to "show me that run". */
98
- function rolledDetail(cell, W) {
99
- const p = palette();
100
- const r = cell.rolled;
101
- if (r.parts !== undefined)
102
- return exploreRows(r.parts, W);
103
- return r.targets.map((t) => ` ${p.dim}└ ${escapeTerminal(t)}${p.reset}`);
104
- }
105
- /**
106
- * W13 / TUI2-R1 (B) — the rollup's own projection of a run: the count,
107
- * the lines, the elapsed, the targets, and — on a MIXED run only — the
108
- * per-tool parts. A single-name run keeps W13's row byte for byte,
109
- * which is the "the generalization adds, it never rewrites" rule.
110
- *
111
- * Extracted at R3b because the segment fold's EXPANSION renders the run
112
- * through this same projection rather than reimplementing it — so the
113
- * expanded rows cannot drift from the ones the commit path would have
114
- * drawn.
115
- */
116
- function rolledOf(members) {
117
- let total = 0;
118
- const targets = [];
119
- for (const m of members) {
120
- // the lines count, excluding the tool's OWN truncation note
121
- // (read_file's "… N more lines") — the per-cell meta's rule
122
- const noteAt = m.resultText.lastIndexOf("\n… ");
123
- const shown = noteAt >= 0 ? m.resultText.slice(0, noteAt) : m.resultText;
124
- const rows = shown.split("\n");
125
- total += rows[rows.length - 1] === "" ? rows.length - 1 : rows.length;
126
- let input = {};
127
- try {
128
- input = JSON.parse(m.inputFull);
129
- }
130
- catch {
131
- // the full JSON is always parseable (stringified at toolStart)
132
- }
133
- const target = toolTarget(m.name, input);
134
- targets.push(target.split("/").pop() ?? target);
135
- }
136
- const parts = exploreParts(members);
137
- const first = members[0];
138
- const last = members[members.length - 1];
139
- const elapsed = first.startedAt !== null && last.doneAt !== null ? ((last.doneAt - first.startedAt) / 1000).toFixed(1) : "?";
140
- return { count: members.length, lines: total, elapsed, targets, ...(parts.length > 1 ? { parts } : {}) };
141
- }
142
- /**
143
- * TUI2-R1 (B) / R3b — the per-tool parts of an explore run, in
144
- * first-call order. A search's subject is the PATTERN it looked for
145
- * (quoted); a read's or a list's is the path it named.
146
- *
147
- * Extracted at R3b because TWO paths need it now: the commit-time
148
- * rollup, which has always built it, and the segment fold's EXPANSION,
149
- * which shows the rollup's rows rather than one row per call. Two
150
- * copies of this would be two answers to "what did that run do".
151
- */
152
- function exploreParts(members) {
153
- const parts = [];
154
- for (const m of members) {
155
- let input = {};
156
- try {
157
- input = JSON.parse(m.inputFull);
158
- }
159
- catch {
160
- // the full JSON is always parseable (stringified at toolStart)
161
- }
162
- const target = toolTarget(m.name, input);
163
- const subject = m.name === "search_text" ? `"${String(input.pattern ?? "")}"` : target;
164
- const part = parts.find((x) => x.name === m.name);
165
- if (part === undefined)
166
- parts.push({ name: m.name, subjects: [subject] });
167
- else
168
- part.subjects.push(subject);
169
- }
170
- return parts;
171
- }
172
- /** R3b — a segment's terms, for the expand header. The fold line's own
173
- * wording comes from `turnFold`; this is the same facts in the header
174
- * idiom the other expands use. */
175
- function foldMeta(seg) {
176
- const parts = foldTerms(seg.reads, seg.edits, [...seg.others]);
177
- return parts.length === 0 ? "thinking" : parts.join(" · ");
178
- }
83
+ /* R13 `rolledTitle`, `rolledDetail` and `rolledOf` retired with the
84
+ W13 rollup and TUI2-R1 (B)'s exploration row (see #foldOrRollup). */
179
85
  /** R3b — the turn's open segment, opened on demand at the first cell of
180
86
  * work that follows a text block (or the turn's start). Returns null
181
87
  * only when there is no turn at all, which is the pipe path's shape. */
@@ -595,7 +501,7 @@ export class Body {
595
501
  }
596
502
  }
597
503
  this.#toolCells.set(callId, this.#cells.length);
598
- this.#cells.push({ kind: "tool", name, input: summary, inputFull: JSON.stringify(input, null, 2), childRoles, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false, expanded: false, turn: this.#turns.length - 1, rolled: null, reason: null, verdict: null });
504
+ this.#cells.push({ kind: "tool", name, input: summary, inputFull: JSON.stringify(input, null, 2), childRoles, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false, expanded: false, turn: this.#turns.length - 1, reason: null, verdict: null });
599
505
  // W14: the turn record's counts — the folded-turn line's terms
600
506
  // (reads = read_file, edits = edit_file, the rest in first-call
601
507
  // order). The CLI's recap counts the same way (edit_file).
@@ -1130,91 +1036,8 @@ export class Body {
1130
1036
  }
1131
1037
  return { lines: chunks.flat(), blocks: blocks - skipped, skipped };
1132
1038
  }
1133
- /**
1134
- * R5 the rows a fold stands for, as a PURE projection.
1135
- *
1136
- * Extracted from expandNext so the transcript viewer and the
1137
- * expand key open the same work by construction rather than by
1138
- * two copies agreeing. It renders cells; it mutates none of
1139
- * them beyond the head.rolled save/restore the rollup path has
1140
- * always used, which does not outlive this synchronous call.
1141
- */
1142
- #foldBody(seg, idx, W, ctx) {
1143
- const p = palette();
1144
- const rows = [];
1145
- let run = [];
1146
- const flush = () => {
1147
- if (run.length === 0)
1148
- return;
1149
- // the same threshold the commit-time rollup uses: below it a
1150
- // "run" is just some rows
1151
- if (run.length > 2) {
1152
- // the run renders through the ROLLUP's own projection —
1153
- // literally the same function the commit path uses — so
1154
- // a single-name run keeps W13's row and a mixed one gets
1155
- // the exploration line, exactly as they would have if the
1156
- // segment had never folded.
1157
- const head = run[0];
1158
- const saved = head.rolled;
1159
- head.rolled = rolledOf(run);
1160
- // the run OPENS. The fold's key already asked to see the
1161
- // work, so what lands is the same rows `ctrl+o` on the
1162
- // run itself would have opened — its title, then its
1163
- // detail — never its collapsed row, which would make the
1164
- // reader press a second time for what the first press
1165
- // was for.
1166
- rows.push(` ${p.dim}${escapeTerminal(rolledTitle(head))}${p.reset}`);
1167
- rows.push(...rolledDetail(head, W));
1168
- head.rolled = saved;
1169
- }
1170
- else {
1171
- for (const c of run)
1172
- rows.push(...cellComponent(c).render(W, ctx));
1173
- }
1174
- run = [];
1175
- };
1176
- // R3f — the expansion covers the WHOLE TURN, every segment.
1177
- //
1178
- // R3d moved the fold to the turn while the expansion kept
1179
- // walking one segment, so a turn that spoke between calls
1180
- // folded to a line claiming `3 reads · 1 edit · 1 shell` whose
1181
- // key opened only the reads: the edit and the shell were on no
1182
- // surface and reachable by no key. That is the one thing this
1183
- // round's own first gate forbids — the work is never
1184
- // unreachable — and it is worse than never folding, because the
1185
- // line names work it then withholds.
1186
- //
1187
- // A run still BREAKS at a non-explore cell, so the segment
1188
- // boundaries survive where they carry meaning (the write that
1189
- // splits two explore runs); they simply no longer bound what
1190
- // the key can reach.
1191
- // DECLARED SUPERSESSION (R3i phase 3) — the expansion covers THIS
1192
- // STRETCH, and only this stretch.
1193
- //
1194
- // R3f widened it to the whole turn, and had to: R3d had made
1195
- // the fold the TURN's while the expansion still walked one
1196
- // segment, so a line claiming `read 3 files · edited 1 file`
1197
- // opened only the reads — work named and then withheld, the one
1198
- // thing this file's first gate forbids. R3i moves the fold back
1199
- // to the stretch, so the pairing is exact again: every stretch
1200
- // has its OWN line and its own key, and each key opens the work
1201
- // its line named. Keeping the turn walk would break the same
1202
- // rule from the other side — two lines, each opening
1203
- // everything, each header describing rows the other also shows.
1204
- for (const j of seg.cells) {
1205
- if (j < idx)
1206
- continue;
1207
- const c = this.#cells[j];
1208
- if (c.kind === "tool" && isExploreTool(c.name)) {
1209
- run.push(c);
1210
- continue;
1211
- }
1212
- flush();
1213
- rows.push(...cellComponent(c).render(W, ctx));
1214
- }
1215
- flush();
1216
- return rows;
1217
- }
1039
+ /* R13 — `#foldBody` retired with the segment fold: with nothing
1040
+ folded there is no body a fold stands for. */
1218
1041
  // ─── R5: the transcript viewer ──────────────────────────────────
1219
1042
  //
1220
1043
  // The viewer occupies the LIVE REGION, exactly as the keys sheet
@@ -1305,14 +1128,8 @@ export class Body {
1305
1128
  const cell = this.#cells[idx];
1306
1129
  if (cell === undefined)
1307
1130
  continue;
1308
- const seg = this.#segmentOf(idx);
1309
- if (seg !== null && seg.headCell === idx) {
1310
- out.push({
1311
- head: stretchLine({ ...this.#stretchTerms(seg), phase: "settled" }, inner)[0] ?? "",
1312
- body: this.#foldBody(seg, idx, inner, ctx),
1313
- });
1314
- continue;
1315
- }
1131
+ // R13 the viewer's FOLD entry retired with the fold: every
1132
+ // entry is now a card, and a card's entry is its own full body.
1316
1133
  if (cell.kind !== "tool")
1317
1134
  continue;
1318
1135
  // the tool card's FULL body — the same rows its own ctrl+o
@@ -1423,86 +1240,14 @@ export class Body {
1423
1240
  // (ADR-0046 — history is never rewritten), exactly as every other
1424
1241
  // expand in this method does, and they are the cells' OWN renders,
1425
1242
  // so the expansion cannot drift from what was folded.
1426
- const seg = this.#segmentOf(idx);
1427
- const foldTurn = (cell.kind === "thinking" || cell.kind === "tool") && cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
1428
- if (seg !== null && foldTurn !== undefined && seg.headCell === idx) {
1429
- const p = palette();
1430
- const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
1431
- const back = `${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
1432
- const W = this.#opts.width();
1433
- const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
1434
- // R3b (owner ruling): the ROLLUP is the expansion. TUI2-R1 built
1435
- // a richer projection of an explore run than a fold line can
1436
- // carry — the per-tool counts, and one row per tool with its
1437
- // subjects — and the segment fold would have retired it by
1438
- // simply arriving first. So the run's own rows are what the key
1439
- // opens: explore tools group the way the rollup groups them,
1440
- // everything else renders as itself.
1441
- // The segment's cells IN ORDER, with consecutive explore tools
1442
- // grouped exactly as the rollup groups them — a write, a shell
1443
- // or anything else BREAKS the run, which is TUI2-R1's own rule
1444
- // and the reason two explore runs on either side of a write
1445
- // stay two runs. Merging every explore tool of the segment
1446
- // would have been simpler and would have quietly deleted that
1447
- // rule.
1448
- const rows = this.#foldBody(seg, idx, W, ctx);
1449
- // the header NAMES the segment. When the segment is exactly one
1450
- // explore run, "explored 8 files · 14 searches" is what that run
1451
- // is called everywhere else in the product, and the header says
1452
- // the same thing rather than a second wording of it.
1453
- // the header states what the SEGMENT did, in the fold line's own
1454
- // terms; each run below states what IT did, in the rollup's. Two
1455
- // scales, one wording each — the header used to borrow the run's
1456
- // sentence, which read as the same run twice.
1457
- // the header names what the FOLD said — the turn's terms — so the
1458
- // line you pressed and the block it opens agree. It used to name
1459
- // segment 1's, which contradicted the fold above it.
1460
- const t = this.#stretchTerms(seg);
1461
- const head = foldTerms(t.calls.find(([n]) => n === "read_file")?.[1] ?? 0, t.calls.find(([n]) => n === "edit_file")?.[1] ?? 0, t.calls.filter(([n]) => n !== "read_file" && n !== "edit_file"));
1462
- // R3i phase 4 — THE FOOTER TELLS THE TRUTH ABOUT THIS PATH.
1463
- //
1464
- // The rows come from the rollup's own projection, whose last
1465
- // row reads `└ ctrl+o collapses` — true where it was written
1466
- // (the LIVE toggle, which really does close again) and false
1467
- // here. A committed row is ink: ADR-0046 forbids rewriting
1468
- // history, so nothing about this block can be taken back. The
1469
- // next press opens the NEXT fold, and the row now says so.
1470
- const closing = rows.length > 0 && /ctrl\+o collapses/.test(rows[rows.length - 1] ?? "");
1471
- const body = closing ? rows.slice(0, -1) : rows;
1472
- return {
1473
- kind: "appended",
1474
- lines: [
1475
- // R4a — the header names the fold in WORDS (its own terms
1476
- // and how far back it is), not by an ordinal. The ordinal
1477
- // existed to be typed and never was; the words were
1478
- // always the part a reader could use.
1479
- `${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
1480
- ...body,
1481
- // R8a: an in-block note takes the block's indent, not a
1482
- // second corner — the corner opens the body above it.
1483
- `${p.dim} end of expansion · ctrl+o opens the one before it${p.reset}`,
1484
- ],
1485
- };
1486
- }
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.
1487
1248
  if (cell.kind !== "tool")
1488
1249
  return { kind: "none" };
1489
- if (cell.rolled !== null) {
1490
- // W13: a rolled-up head expands to the FULL per-call children —
1491
- // the rollup showed the first 3 + the overflow; the expand shows
1492
- // every target, one └ row each (the /last idiom — the children
1493
- // land as NEW content, history is never rewritten, ADR-0046).
1494
- const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
1495
- const p = palette();
1496
- const back = `${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
1497
- // TUI2-R1 (B): an EXPLORATION head lists per TOOL — the counts
1498
- // the row showed, then one row per tool with its subjects. The
1499
- // header keeps W15's shape; only the subject changes.
1500
- const rolledHead = rolledTitle(cell);
1501
- return {
1502
- kind: "appended",
1503
- lines: [`${p.bold}✦${p.reset} expanded · ${escapeTerminal(rolledHead)} · ${back}`, ...rolledDetail(cell, this.#opts.width())],
1504
- };
1505
- }
1250
+ // R13 and the ROLLUP branch retired with `rolled`.
1506
1251
  let input = {};
1507
1252
  try {
1508
1253
  input = JSON.parse(cell.inputFull);
@@ -1999,79 +1744,76 @@ export class Body {
1999
1744
  * settle still produces the same fold it did before. That is the
2000
1745
  * charter's line between this phase and the next.
2001
1746
  */
1747
+ /* DC-46 — `#liveRoom` RETIRED, and the reasoning with it.
1748
+ It capped the live region at `H − chrome − #committedLines`, to
1749
+ keep `skip` a function of `#committedLines` alone and therefore
1750
+ monotone. The argument was right about `skip` and wrong about the
1751
+ quantity: `#committedLines` is CUMULATIVE — re-derived over the
1752
+ whole line cache every frame, counting rows that left for the
1753
+ terminal's scrollback long ago — so one screenful into any session
1754
+ it exceeds H, the room clamps to its floor of one row, and every
1755
+ running call after that is a head row with its output gone.
1756
+ Measured at 24 and 40 rows with eight blank rows still on screen.
1757
+
1758
+ The live region is bounded by the SCREEN (the content cap the
1759
+ force-commit loop already uses). What holds the window's top is
1760
+ the `skip` clamp below: rows in [0, #scrolledOff) have reached the
1761
+ terminal's scrollback and are immutable, so the paint may not go
1762
+ back above them. A live region that GROWS scrolls committed rows
1763
+ away through #emitScroll, which is an append and not an un-scroll.
1764
+ The residue is a transient hole above the composer at a settle,
1765
+ bounded by the shrink itself and filled by the next commit. */
2002
1766
  #liveProjection(W, ctx, cap) {
2003
- const rows = this.#project(W, ctx, ACT_SLOT_ROWS);
1767
+ const rows = this.#project(W, ctx, CAP_PREVIEW);
2004
1768
  if (cap === undefined || rows.length <= cap)
2005
1769
  return rows;
2006
- // R4the slot gives way BEFORE any cell is force-committed.
2007
- // A standing slot that could overflow the content cap would make
2008
- // the force-commit loop push REAL cells into the scrollback to
2009
- // relieve rows that are, at the bottom of the slot, blank padding.
2010
- // So the slot shrinks first, in the pinned order slotPad already
2011
- // implements (the pad rows are last, so they go first, then the
2012
- // tail, then the heads beyond the first) and the floor is one row.
2013
- return this.#project(W, ctx, Math.max(1, ACT_SLOT_ROWS - (rows.length - cap)));
1770
+ // DC-43 / R13 E2 THE WINDOW SHRINKS TO THE ROOM, one row at a
1771
+ // time, before any cell is force-committed. A running card that
1772
+ // could overflow the content cap would make the force-commit loop
1773
+ // push REAL cells into the scrollback to relieve rows that are, at
1774
+ // the bottom of a card's window, blank padding. So the cards give
1775
+ // way first, down to a single preview row, and then to their head
1776
+ // rows alone the one form that fits anywhere.
1777
+ //
1778
+ // Committed cards are never trimmed: this is the LIVE projection,
1779
+ // and a row in the scrollback is final (§7.1).
1780
+ for (let n = CAP_PREVIEW - 1; n >= 1; n -= 1) {
1781
+ const tighter = this.#project(W, ctx, n);
1782
+ if (tighter.length <= cap)
1783
+ return tighter;
1784
+ }
1785
+ return this.#project(W, ctx, 0);
2014
1786
  }
2015
- /** R4 — one pass of the live projection at a given slot budget. */
1787
+ /**
1788
+ * R13 — ONE PASS, AND EVERY CELL RENDERS ITSELF.
1789
+ *
1790
+ * DECLARED REVERSAL of R3i's stretch line, R4's standing activity
1791
+ * slot and R6/D1's block-stands-for-the-turn, all owner-ruled on
1792
+ * 2026-09-03 and all of them the same idea: the open stretch drew ONE
1793
+ * line plus a fixed slot, and every other cell of the segment drew
1794
+ * nothing, so the live region's height was independent of the call
1795
+ * count. That was the answer to a 28-call turn spending 28 rows of a
1796
+ * 30-row region.
1797
+ *
1798
+ * The card answers it differently, and the ruling prefers this
1799
+ * answer: a running call is its own card at a FIXED height (E2), so
1800
+ * the height is a function of how many calls are IN FLIGHT rather
1801
+ * than of how many have happened — and the window shrinks to the room
1802
+ * before anything is force-committed (above). The live form and the
1803
+ * committed form are now the same form, which is what makes a settle
1804
+ * a change of content and never of position.
1805
+ *
1806
+ * What this keeps from R4: the height never moves ON ITS OWN. What it
1807
+ * gives up: the one-line summary of a stretch, which the owner ruled
1808
+ * costs more than it buys.
1809
+ */
2016
1810
  #project(W, ctx, budget) {
2017
1811
  const out = [];
2018
1812
  const focus = this.#focusIndex();
2019
- const turn = this.#turns[this.#turns.length - 1];
2020
- const open = turn !== undefined && !turn.ended ? (turn.segments[turn.segments.length - 1] ?? null) : null;
2021
- const openSeg = open !== null && open.closedAt === null ? open : null;
1813
+ const live = { ...ctx, liveWindow: budget };
2022
1814
  let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
2023
- let stretchDrawn = false;
2024
- let lastIdx = this.#committed;
2025
1815
  for (let i = this.#committed; i < this.#cells.length; i += 1) {
2026
- const cell = this.#cells[i];
2027
- lastIdx = i;
2028
- const inOpen = openSeg !== null && openSeg.cells.includes(i);
2029
- if (inOpen) {
2030
- // R4 — the open stretch is ONE contiguous block: its line
2031
- // plus the standing act slot, spaced once, at the segment's
2032
- // first live cell. Every other cell of the segment draws
2033
- // nothing; its work is counted on the line and its output,
2034
- // if it is the current thing, is in the slot.
2035
- //
2036
- // R3i drew the line here and then let each cell decide for
2037
- // itself whether it still had rows — which is why the
2038
- // region's height moved between every pair of calls.
2039
- if (stretchDrawn)
2040
- continue;
2041
- stretchDrawn = true;
2042
- // R7a — a ONE-CELL stretch draws NO line.
2043
- //
2044
- // It said `running 1 shell command` directly above a row
2045
- // reading `● shell npm run check`: the same fact twice, and
2046
- // fable's R4 review had already named the duplication. R7
2047
- // then made it a SWALLOW as well — a one-cell segment does
2048
- // not fold, so that line has no committed counterpart and
2049
- // vanished at the settle, taking a row off the screen. The
2050
- // call's own head row is the line; a summary of one thing
2051
- // is the thing.
2052
- const single = openSeg.cells.filter((j) => this.#cells[j]?.kind === "tool").length <= 1;
2053
- const rows = [
2054
- ...(single
2055
- ? []
2056
- : stretchLine({ ...this.#stretchTerms(openSeg), liveNames: this.#liveNames(openSeg), phase: this.#stretchPhase(openSeg), ...(this.#inFlight(openSeg) ? { mark: breathFrame(this.#spinnerI) } : {}) }, W)),
2057
- ...this.#actSlot(openSeg, W, ctx, budget, focus),
2058
- ];
2059
- // R7a — the block TAKES the W11 blank, like everything else.
2060
- //
2061
- // CORRECTION of my own R6/D1 change, which removed it. The
2062
- // reasoning then was that the committed fold is a single row
2063
- // and single rows take no blank — but `bodySpacing` gives a
2064
- // blank after any MULTI-row sibling, and the thinking block
2065
- // above the work is exactly that. So the committed side had
2066
- // one and the live side did not, and a blank row APPEARED at
2067
- // every settle, shoving everything below it down. The owner
2068
- // saw it and said the blank is the correct form; it is, and
2069
- // the fix is to have it on both sides rather than neither.
2070
- out.push(...this.#blockSpace(i, prev, rows));
2071
- prev = rows;
2072
- continue;
2073
- }
2074
- const rows = cellComponent(cell).render(W, ctx);
1816
+ const rows = cellComponent(this.#cells[i]).render(W, live);
2075
1817
  // the head row carries the affordance; the tint lands on it and
2076
1818
  // nowhere else, which is what makes "exactly one" structural
2077
1819
  if (i === focus && rows.length > 0)
@@ -2079,324 +1821,8 @@ export class Body {
2079
1821
  out.push(...this.#space(i, prev, rows));
2080
1822
  prev = rows;
2081
1823
  }
2082
- // R6/D1 — THE BLOCK STANDS FOR THE TURN, not for the stretch.
2083
- //
2084
- // R4 made the block's height constant WITHIN a stretch. Between
2085
- // two stretches it was released and rebuilt, so the live region
2086
- // breathed by its whole height twice per stretch — and the live
2087
- // region is anchored to the bottom (liveTop below), so every
2088
- // committed row on screen moved with it. The owner's report, and
2089
- // their own formulation of the cure: "once something is at a line
2090
- // it should not jump up or down — hold the absolute position and
2091
- // update the content there."
2092
- //
2093
- // So when the turn has begun work and no open stretch drew the
2094
- // block above, it is drawn HERE, after the live cells, with its
2095
- // top row swapped to the slot's own pad: the closed stretch's
2096
- // terms are already the committed fold row further up, and
2097
- // printing them again one screen apart is the duplication A9's
2098
- // narrowing forbids.
2099
- //
2100
- // Projection-only. Commit order, commit timing and every
2101
- // committed byte are untouched — which is why the transcript this
2102
- // leaves behind cannot regress: it cannot differ.
2103
- if (!stretchDrawn && turn !== undefined && !turn.ended && turn.begun) {
2104
- // R7a: the top row is BLANK, not a `│`. It stands where the
2105
- // stretch line stands while a stretch is open, and between two
2106
- // stretches there is no line to draw — a bare gutter there is
2107
- // a mark on a row with nothing to mark (law 1.3), and it is
2108
- // the "long vertical line" the owner saw under a finished
2109
- // turn. The ROW is what holds the height; the glyph never was.
2110
- const rows = ["", ...this.#actSlot(null, W, ctx, budget, focus)];
2111
- out.push(...this.#blockSpace(lastIdx, prev, rows)); // R7a: the same blank
2112
- }
2113
1824
  return out;
2114
1825
  }
2115
- /** R6/D1 — the turn's most recent CLOSED segment: what the block
2116
- * shows in the gap between two stretches. */
2117
- #lastClosedSegment() {
2118
- const turn = this.#turns[this.#turns.length - 1];
2119
- if (turn === undefined)
2120
- return null;
2121
- for (let i = turn.segments.length - 1; i >= 0; i -= 1) {
2122
- const seg = turn.segments[i];
2123
- if (seg.cells.some((j) => j >= this.#committed))
2124
- return seg;
2125
- }
2126
- return null;
2127
- }
2128
- /**
2129
- * R4 — the standing act slot's rows. EXACTLY `budget` rows in every
2130
- * phase, so the live region's height changes twice per stretch (once
2131
- * when it opens, once when it folds) instead of twice per call.
2132
- *
2133
- * The phases, in the order they are tested:
2134
- * - an EXPANDED live cell outranks the slot (W15 — "the user asked
2135
- * for it"): it renders in full, variable height. This is also
2136
- * DC-28's cure: mid-stretch `ctrl+o` had a target it toggled and
2137
- * never drew, so the press did nothing visible now and changed a
2138
- * later expansion's shape;
2139
- * - CALLS IN FLIGHT: one head row each within the budget, the tail
2140
- * of the LAST head shown filling what is left, and the overflow
2141
- * row inside the slot. The tail belongs to the last head by
2142
- * construction — never call N's output under call N+1's header;
2143
- * - the GAP between two calls: the call that just finished keeps its
2144
- * settled head and its tail. This is the frame R3i collapsed, and
2145
- * collapsing it is most of the jump;
2146
- * - THINKING, before any call: the thinking's own tail (R3i ruling
2147
- * 5, wired at last).
2148
- */
2149
- #actSlot(seg, W, ctx, budget, focus) {
2150
- const tint = (i, rows) => {
2151
- if (i === focus && rows.length > 0)
2152
- rows[0] = focusToken(rows[0], W);
2153
- return rows;
2154
- };
2155
- // R6/D1: with no open stretch (the gap BETWEEN two of them) the
2156
- // slot looks at the turn's last closed segment instead — the call
2157
- // that just finished keeps its head and its tail, which is R4 B's
2158
- // rule extended across the boundary. Cells outlive their commit,
2159
- // so these are live repaints of live rows, never a rewrite of a
2160
- // committed one.
2161
- const src = seg ?? this.#lastClosedSegment();
2162
- if (src === null)
2163
- return [];
2164
- const live = src.cells.filter((i) => i >= this.#committed);
2165
- const tools = [];
2166
- for (const i of live)
2167
- if (this.#cells[i]?.kind === "tool")
2168
- tools.push(i);
2169
- const toolAt = (i) => this.#cells[i];
2170
- // An APPROVAL and an EXPANSION both outrank the slot, for the same
2171
- // reason: their height is the human's business, not the renderer's.
2172
- // W21 gives a pending approval the live region wholesale — its
2173
- // diff is the thing being decided about, and a diff clamped to
2174
- // four rows is a decision made on partial evidence. W15 gives an
2175
- // expanded cell its full body — "the user asked for it". The slot
2176
- // exists to stop the height moving ON ITS OWN; a height a human
2177
- // asked for is not the oscillation it was built against.
2178
- //
2179
- // (The approval half is a regression this round caused and its
2180
- // gate caught: the first draft treated a pending approval as a
2181
- // call in flight, so `❯ edit x.ts` lost its diff tail and the
2182
- // `ctrl+o to expand` note with it.)
2183
- const owned = tools.filter((i) => toolAt(i).expanded || toolAt(i).state === "approval");
2184
- if (owned.length > 0) {
2185
- // In CELL ORDER, so the frame reads the way the work happened:
2186
- // an owned cell in full, every OTHER call still in flight
2187
- // keeping its head row. An approval pausing one call must never
2188
- // hide the others — the v2d parallel-frame gate caught exactly
2189
- // that: with the shell running and asky_read at its panel, the
2190
- // first draft returned the panel alone and the running shell's
2191
- // `● shell sleep 1; echo hi · 1s` row vanished from the screen.
2192
- const shown = tools.filter((i) => owned.includes(i) || !toolAt(i).done);
2193
- const out = [];
2194
- let heads = 0;
2195
- for (const i of shown) {
2196
- const rows = tint(i, cellComponent(this.#cells[i]).render(W, ctx));
2197
- if (owned.includes(i)) {
2198
- out.push(...rows);
2199
- continue;
2200
- }
2201
- if (heads >= LIVE_ACT_HEADS)
2202
- continue;
2203
- heads += 1;
2204
- out.push(rows[0] ?? "");
2205
- }
2206
- const hidden = shown.length - owned.length - heads;
2207
- if (hidden > 0)
2208
- out.push(moreRunningRow(hidden, W));
2209
- return out;
2210
- }
2211
- // R7a — ONE PATH, whether or not anything is in flight.
2212
- //
2213
- // There used to be two: the in-flight composition, and a
2214
- // "last finished call plus its output" composition for the gap
2215
- // between stretches. The moment the last call of a burst
2216
- // returned, the block re-composed from four head rows to one
2217
- // head and a tail — a row shorter, so on a full screen every
2218
- // row above slid DOWN. The block is supposed to change its
2219
- // CONTENTS, not its shape; the last call returning is not a
2220
- // reason to redraw the stretch differently.
2221
- if (tools.length > 0) {
2222
- // the one-call special case is GONE: the path below draws a
2223
- // lone running call by its own component (the W8 block
2224
- // verbatim, which is what 0.17.0 drew) and a lone finished
2225
- // one as its head plus its output. The special case only
2226
- // differed once the call SETTLED, where it collapsed to the
2227
- // bare head row — a three-row shrink the moment a single
2228
- // call returned.
2229
- // R7a — EVERY call of the stretch keeps its row, not just the
2230
- // ones still in flight.
2231
- //
2232
- // R4 showed the in-flight calls only, so a finished one left
2233
- // the block and its target went with it: a four-file burst
2234
- // ended having shown four names and left none of them, while
2235
- // the rows below shuffled up one at a time. Two complaints in
2236
- // one — "I can't see what it read" and "the rows keep moving".
2237
- //
2238
- // A call now takes a row when it STARTS and changes in place
2239
- // when it finishes: `● read x · 1s` becomes the settled head.
2240
- // Nothing moves, every target stays, and exactly ONE row wears
2241
- // the breathing mark — the running one — which is the mark's
2242
- // whole job (§7.4: only the call still running carries one,
2243
- // because only it is moving).
2244
- //
2245
- // The slot's fixed height is what pays for this: the rows are
2246
- // already allocated, so the names fill blanks rather than
2247
- // pushing anything.
2248
- // TRUNCATION NEVER DROPS A CALL THAT IS STILL RUNNING.
2249
- //
2250
- // Taking the first N is wrong the moment a burst outlives the
2251
- // slot: four reads that finished held every row while the
2252
- // shell still running was cut, so the screen said "4 files"
2253
- // and showed nothing of the work actually in flight. The
2254
- // in-flight set is admitted first, then the most RECENT
2255
- // finished calls fill what is left — newest first, because
2256
- // the oldest is the one the eye has already read.
2257
- const live = tools.filter((i) => !toolAt(i).done);
2258
- const past = tools.filter((i) => toolAt(i).done);
2259
- // WHAT IS HAPPENING NOW OUTRANKS WHAT HAPPENED. In order:
2260
- // the in-flight rows, then that call's output when it is the
2261
- // only one running, then the finished NAMES, newest first.
2262
- //
2263
- // This is the line between R3i P1 and the owner's R7a ruling,
2264
- // which look contradictory and are not. The ruling is about a
2265
- // parallel burst — four reads whose names vanished one at a
2266
- // time, so the turn ended having shown four files and left
2267
- // none of them. P1 is about a burst that is OVER and a new
2268
- // call running: there the finished names have had their time
2269
- // on screen and the work in flight has not. Recency decides
2270
- // both, and neither gate has to give.
2271
- // the lone in-flight call is drawn by its OWN component, head
2272
- // and tail together — that is where the waiting row, VD-4's
2273
- // never-blank-first-row rule and the shell's live window all
2274
- // already live. Reaching past it to slotTail() lost every one
2275
- // of them: a running shell with no output yet drew three
2276
- // blank rows where `└ waiting for output` belongs.
2277
- // `grouped` says "an activity line above wears the mark for
2278
- // us". A stretch of ONE call draws no such line (R7a), so
2279
- // there is nothing above to carry it and the head keeps its
2280
- // own — otherwise a lone running call breathes nowhere.
2281
- const grouped = { ...ctx, grouped: tools.length > 1 };
2282
- const soloRows = live.length === 1 ? tint(live[0], cellComponent(this.#cells[live[0]]).render(W, grouped)) : [];
2283
- const tailWant = Math.max(0, soloRows.length - 1);
2284
- // the overflow row is itself a row: an in-flight set larger
2285
- // than the slot gives one back so `+N more running` fits.
2286
- const liveRows = live.slice(0, live.length > budget ? Math.max(1, budget - 1) : budget);
2287
- const spare = Math.max(0, budget - liveRows.length - tailWant);
2288
- const nameRoom = past.length > spare ? Math.max(0, spare - 1) : spare;
2289
- const keep = new Set([...liveRows, ...past.slice(past.length - nameRoom)]);
2290
- const shown = tools.filter((i) => keep.has(i));
2291
- // `+N more running` COUNTS ONLY CALLS THAT ARE RUNNING.
2292
- //
2293
- // Counting every dropped call said "+1 more running" over a
2294
- // read that had already returned — a false sentence of the
2295
- // R3h class, and the stretch line above had ALREADY counted
2296
- // that read ("read 1 file"), so the row was both wrong and
2297
- // redundant. A finished name giving way to live work is the
2298
- // recency rule doing its job, not an overflow.
2299
- const hidden = live.length - shown.filter((i) => !toolAt(i).done).length;
2300
- const rows = [];
2301
- // the mark lives on the ACTIVITY line above, so the members
2302
- // wear a plain gutter — see FrameCtx.grouped.
2303
- for (const i of shown) {
2304
- if (i === live[0] && live.length === 1)
2305
- rows.push(...soloRows.slice(0, Math.max(1, budget - rows.length)));
2306
- else
2307
- rows.push(tint(i, cellComponent(this.#cells[i]).render(W, grouped))[0] ?? "");
2308
- }
2309
- if (hidden > 0)
2310
- rows.push(moreRunningRow(hidden, W));
2311
- // R3i P3 SURVIVES: the call in flight keeps its row AND its
2312
- // output. R7a gave every call a row, which spent the budget
2313
- // the tail used to hold — but a running shell with no output
2314
- // on screen is the defect R3i named, and the owner's ruling
2315
- // was about the finished calls' NAMES, not about this. The
2316
- // tail takes whatever the head rows leave, so it is full
2317
- // height for a lone call and gives way to the names first.
2318
- // R4 B SURVIVES THE UNIFICATION: between two calls — nothing
2319
- // in flight — the slot still shows the call that just
2320
- // finished AND its output. It is appended UNDER the head
2321
- // rows now instead of replacing them, so the block's shape
2322
- // does not change when the last call of a burst returns.
2323
- const rest = budget - rows.length;
2324
- if (live.length === 0 && rest > 0 && tools.length > 0)
2325
- rows.push(...slotTail(toolAt(tools[tools.length - 1]).resultText, W, rest));
2326
- return slotPad(rows, budget);
2327
- }
2328
- const think = [...live].reverse().find((i) => this.#cells[i]?.kind === "thinking");
2329
- // R7a — A SLOT WITH NOTHING TO SHOW TAKES NO ROWS.
2330
- //
2331
- // R7 moved thinking OUT of the slot (it is words, and words do
2332
- // not fold), which left this branch — the pre-tool phase of a
2333
- // stretch — with nothing to put in the rows it was still
2334
- // reserving. It padded them anyway: six blank rows between the
2335
- // thought and the composer, on 653 of a 733-frame dogfood
2336
- // replay. Until today those rows were drawn as `│`, so the
2337
- // blank-run guard never saw them and the owner saw a gutter
2338
- // running down the screen marking nothing; blanking the gutter
2339
- // (law 1.3) revealed the hole the gutter had been covering.
2340
- //
2341
- // Reserving height buys stability only where the content
2342
- // CHANGES under it — a stretch whose calls come and go. Before
2343
- // the first call there is nothing to stabilise, so the rows are
2344
- // pure cost, and both complaints are the same complaint.
2345
- const tail = think === undefined ? [] : slotTail(this.#cells[think].text, W, budget);
2346
- return tail.length === 0 ? [] : slotPad(tail, budget);
2347
- }
2348
- /** R4 — the tool names with a call still IN FLIGHT in this segment.
2349
- * The stretch line's tense is per term, so a finished shell reads
2350
- * `ran 1 shell command` while a read is still running. */
2351
- #liveNames(seg) {
2352
- const names = new Set();
2353
- for (const i of seg.cells) {
2354
- const c = this.#cells[i];
2355
- if (c !== undefined && c.kind === "tool" && !c.done)
2356
- names.add(c.name);
2357
- }
2358
- return [...names];
2359
- }
2360
- /** R3i — the open stretch's phase. It is THINKING while a thinking
2361
- * cell of it is still open and no call has started; otherwise it is
2362
- * ACTING. The tense follows the phase, and the phase is what the
2363
- * human is watching happen. */
2364
- #stretchPhase(seg) {
2365
- return seg.thinkingSince !== null && seg.reads === 0 && seg.edits === 0 && seg.others.size === 0 ? "thinking" : "acting";
2366
- }
2367
- /** R3i — the open stretch's terms, in the shape the line renders. */
2368
- #stretchTerms(seg) {
2369
- const ms = seg.thinkingMs + (seg.thinkingSince === null ? 0 : Math.max(0, Date.now() - seg.thinkingSince));
2370
- // R3i: a call in TROUBLE does not contribute to the work terms.
2371
- // The counts are taken at toolStart, before the outcome is known,
2372
- // so a denied write would otherwise fold as `wrote 1 file` beside
2373
- // the clause admitting it was refused — the line saying, in one
2374
- // breath, that the file was written and that it was not. The
2375
- // trouble clause is where those calls are counted.
2376
- const bad = new Map();
2377
- for (const j of seg.cells) {
2378
- const c = this.#cells[j];
2379
- if (c === undefined || c.kind !== "tool" || !this.#cellInTrouble(j))
2380
- continue;
2381
- bad.set(c.name, (bad.get(c.name) ?? 0) + 1);
2382
- }
2383
- const net = (name, n) => Math.max(0, n - (bad.get(name) ?? 0));
2384
- const calls = [];
2385
- if (net("read_file", seg.reads) > 0)
2386
- calls.push(["read_file", net("read_file", seg.reads)]);
2387
- if (net("edit_file", seg.edits) > 0)
2388
- calls.push(["edit_file", net("edit_file", seg.edits)]);
2389
- for (const [name, n] of seg.others)
2390
- if (net(name, n) > 0)
2391
- calls.push([name, net(name, n)]);
2392
- const targets = [];
2393
- for (const j of seg.cells) {
2394
- const c = this.#cells[j];
2395
- if (c !== undefined && c.kind === "tool")
2396
- targets.push(toolTarget(c.name, JSON.parse(c.inputFull)));
2397
- }
2398
- return { thoughtSeconds: Math.round(ms / 1000), calls, targets, trouble: this.#segmentTroubleTerms(seg) };
2399
- }
2400
1826
  /** The live region's scalar — the unit tests assert the cap directly
2401
1827
  * (the e2e gate pins the screen consequence). W11: the formula's
2402
1828
  * blanks are join artifacts — the count includes them (they are real
@@ -2803,63 +2229,19 @@ export class Body {
2803
2229
  // composer (N = 1 ⇒ H−2, the retired hard-coded anchor).
2804
2230
  this.#lastAnchorRow = H - 1 - editor.rows.length + editor.markerRow;
2805
2231
  }
2806
- /** TUI2-MD — the join blank between cell i−1 and cell i.
2807
- *
2808
- * W11's formula ("a blank above a row that is itself a block, or
2809
- * whose previous sibling was taller than one row") reads ROW COUNTS,
2810
- * and markdown's rhythm is not a row count: a heading wants a blank
2811
- * above and below it even between two one-row paragraphs, and two
2812
- * rows of one fence want none even when a long code line folds to
2813
- * two. So between two MARKDOWN cells the formula steps aside and the
2814
- * block's own `gap` decides — the renderer owns the rhythm, which is
2815
- * the only place that knows it. Every other pair is untouched,
2816
- * including the boundary INTO a markdown message (the blank under the
2817
- * user chip is still W11's). */
2818
- /** R7a — the live block's spacing is the spacing its COMMITTED form
2819
- * will get, never its own.
2820
- *
2821
- * W11 gives a blank when either side is multi-row. The block is
2822
- * always multi-row and the fold it commits into is always ONE row,
2823
- * so the two sides disagreed by construction and a blank appeared
2824
- * or vanished at every settle, shoving the whole transcript by a
2825
- * row. Both directions occur: after a two-row thought the settle
2826
- * ADDED one (my R6/D1 note saw only this case and removed the
2827
- * block's blank, which fixed that direction and broke the other);
2828
- * after a one-row thought it REMOVED one.
2829
- *
2830
- * Deciding on a one-row stand-in makes the block spaced exactly as
2831
- * its fold will be, so the settle changes the row's CONTENT and
2832
- * never its position — which is the whole claim of the standing
2833
- * block. */
2834
- /** R7a — is any call of this stretch actually RUNNING?
2835
- *
2836
- * The phase is not the same question. A stretch stays "acting" from
2837
- * its first tool to its close, so between two bursts — every call
2838
- * returned, the model is composing the next one — the phase still
2839
- * said acting and the activity line went on breathing over four
2840
- * finished reads. A mark that is lit when nothing moves is the
2841
- * spinner-implies-progress error §5.3 forbids, one scale up. */
2842
- #inFlight(seg) {
2843
- return seg.cells.some((i) => { const c = this.#cells[i]; return c?.kind === "tool" && !c.done; });
2844
- }
2232
+ /** R13 D1 — the same constant as everything else. This used to compute
2233
+ * the lead from a ONE-ROW STAND-IN so the live block would claim the
2234
+ * spacing its fold was going to get; with the rhythm constant there
2235
+ * is nothing to simulate, and R7a's device retires with the formula
2236
+ * that needed it. */
2845
2237
  #blockSpace(i, prev, rows) {
2846
- const lead = bodySpacing(this.#lastDrawn(i, prev), ["x"]).length > 1 ? [""] : [];
2847
- return [...lead, ...rows];
2238
+ return bodySpacing(this.#lastDrawn(i, prev), rows);
2848
2239
  }
2849
2240
  #space(i, prev, rows) {
2850
2241
  if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
2851
2242
  return rows;
2852
- // R7a, extended to the SETTLED side (R9 P2 / D4). R7a made the LIVE
2853
- // block claim the spacing its fold will take, so a settle changed a
2854
- // row's content and not its position. That held while a settled
2855
- // tool cell was one row; D4 makes it five, and `bodySpacing` gives a
2856
- // blank for a multi-row cell — so the blank appeared at the settle
2857
- // and pushed the head row down by one, which is the exact defect
2858
- // R7a closed. A tool cell is spaced by the ONE-ROW stand-in on both
2859
- // sides now: the fold is what every form of this cell is spaced
2860
- // against, live, settled and folded alike.
2861
- if (this.#cells[i]?.kind === "tool")
2862
- return [...bodySpacing(this.#lastDrawn(i, prev), ["x"]).slice(0, -1), ...rows];
2243
+ // R13 D1 retires the tool cell's stand-in: the rhythm is a constant,
2244
+ // so a settle changes content and never position by construction.
2863
2245
  return bodySpacing(this.#lastDrawn(i, prev), rows);
2864
2246
  }
2865
2247
  /**
@@ -3042,28 +2424,6 @@ export class Body {
3042
2424
  if (seg !== null)
3043
2425
  seg.spilled = true;
3044
2426
  }
3045
- /** R3f — did ANY of the turn's segments spill? The fold is the
3046
- * TURN's, so one spilled segment makes the whole turn unfoldable:
3047
- * a line claiming the turn's counts cannot stand under rows that
3048
- * already show part of that same work. */
3049
- #turnSpilled(turn) {
3050
- return turn.segments.some((seg) => seg.spilled);
3051
- }
3052
- /** R3d — the turn's cells and its trouble, across every segment. */
3053
- #turnCells(turn) {
3054
- let n = 0;
3055
- for (const seg of turn.segments)
3056
- n += seg.cells.length;
3057
- return n;
3058
- }
3059
- #turnHasTrouble(turn) {
3060
- return turn.segments.some((seg) => this.#segmentHasTrouble(seg));
3061
- }
3062
- /** R3b — how many cells the segment holds. The fold's threshold reads
3063
- * it; nothing else needs it, so it is counted rather than tracked. */
3064
- #segmentCells(seg) {
3065
- return seg.cells.length;
3066
- }
3067
2427
  /** R3b — the segment a committed cell belongs to, or null when it has
3068
2428
  * none (a cell of the pipe path, or a kind that is not work). */
3069
2429
  #segmentOf(i) {
@@ -3082,109 +2442,29 @@ export class Body {
3082
2442
  * of a released turn commits normally). The force-commit path never
3083
2443
  * consults this — the screen's hard cap wins over the hold. */
3084
2444
  #held(i) {
3085
- const cell = this.#cells[i];
3086
- if (cell.kind !== "thinking" && cell.kind !== "tool")
3087
- return false;
3088
- // R3i phase 5: an answered question is WORDS (law 1.7). It commits
3089
- // when it is done, like prose, and is never held for a fold that
3090
- // is not going to speak for it.
3091
- if (cell.kind === "tool" && cell.name === "ask_user")
3092
- return false;
3093
- // R7: and neither is THINKING. It is words now (law 1.7 — work
3094
- // folds, words do not), so it commits when it is done, like prose,
3095
- // and is never held for a fold that no longer speaks for it. The
3096
- // ask_user exemption one line up is the precedent this follows.
3097
- if (cell.kind === "thinking")
3098
- return false;
3099
- const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
3100
- if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
3101
- return false;
3102
- // R3b (owner, 2026-08-27) — the hold is the SEGMENT's, not the
3103
- // quiet turn's.
3104
- //
3105
- // W14 held a turn's work only while the turn had produced no text
3106
- // at all, because the fold existed only for a turn that never
3107
- // spoke. The owner ruled that a segment folds the moment text
3108
- // arrives, so the unit whose committed form is undecided is the
3109
- // SEGMENT: while it is open, its cells must not reach the
3110
- // scrollback, because a committed row cannot be replaced by the
3111
- // fold line that is going to stand for it.
2445
+ // R13 NOTHING IS HELD ANY MORE. DECLARED REVERSAL of W14's
2446
+ // quiet-turn hold, R3b/R3i's segment hold and TUI2-R1.5 ①'s
2447
+ // explore-run hold, all three of them owner-ruled away on
2448
+ // 2026-09-03 with the mechanism they served.
3112
2449
  //
3113
- // This is exactly the change design.md §8 warned about "folding
3114
- // at every text boundary changes what commits and when" and the
3115
- // warning is why the hold is stated here, once, rather than
3116
- // spread across the callers.
2450
+ // Every one of them existed for the same reason: a done cell's
2451
+ // COMMITTED FORM was undecided while its segment or its run was
2452
+ // still open, because a fold line or a rollup row might yet stand
2453
+ // for it and a committed row cannot be taken back (ADR-0046).
2454
+ // With no fold and no rollup, a call's committed form is its own
2455
+ // card and is settled the instant the call is: there is nothing
2456
+ // left to wait for.
3117
2457
  //
3118
- // The quiet turn is the same rule seen from one side: its single
3119
- // segment never closes until the settle, so it holds exactly as
3120
- // it always did.
3121
- // DECLARED SUPERSESSION (R3i phase 3, owner-ruled) THE HOLD IS
3122
- // THE SEGMENT'S AGAIN.
3123
- //
3124
- // R3d had made it the TURN's, so a turn's whole work waited on the
3125
- // settle and every one of its counts then landed on ONE line above
3126
- // all of its prose. The shape the owner asked for is one summary
3127
- // per stretch, standing with the prose that stretch led to — which
3128
- // requires a stretch to commit when its own text arrives.
3129
- //
3130
- // R3d's stated reason for leaving the segment was R3b's disease (a
3131
- // chatty model turning every call into its own `✦ thought 2s ·
3132
- // 1 read` row); the cures are the two rules R3b never had — a fold
3133
- // must absorb at least two rows, and a stretch of exactly one call
3134
- // names its TARGET rather than its count.
3135
- //
3136
- // The force-commit path still overrides this (a stretch too big for
3137
- // the screen spills and renders normally); that is the honest
3138
- // degradation, marked `spilled`.
3139
- const seg = this.#segmentOf(i);
3140
- if (seg !== null)
3141
- return seg.closedAt === null;
3142
- // no segment (the pipe path's shape) — W14's original test, kept
3143
- // so a cell that never got a segment behaves as it used to.
3144
- if (!turn.ended && !turn.hasText)
3145
- return true;
3146
- if (turn.ended)
3147
- return false;
3148
- return this.#growingRun(i);
3149
- }
3150
- /** TUI2-R1.5 ① (VD-1) — the explore-run hold. W14's hold covers the
3151
- * QUIET turn only, and the model's own narration ("let me look at the
3152
- * parser area") sets hasText before the first read even starts: from
3153
- * there each completion committed in its OWN frame, the head committed
3154
- * alone, and `members.every(done)` — the fold's gate — could never be
3155
- * true again. Every real session therefore degraded to one row per
3156
- * call while the unit suite, which feeds the burst synchronously,
3157
- * stayed green (the walkthrough's frame s1-06).
3158
- *
3159
- * The hold is the smallest honest fix: a DONE explore cell whose run
3160
- * can still GROW does not commit yet — its committed form is not
3161
- * decided until the run is closed. The run closes at the first
3162
- * non-explore cell (the model's next word, an edit, a shell) or at the
3163
- * turn's end, and the whole run then commits in ONE frame, which is
3164
- * exactly the shape the fold was written for.
3165
- *
3166
- * The force-commit path never consults this (see #held's callers): the
3167
- * screen's hard cap still wins, so the screen never sticks — a run
3168
- * under real screen pressure degrades mid-turn, and the rows it
3169
- * already froze stay frozen (history is never rewritten, ADR-0046). */
3170
- #growingRun(i) {
3171
- const cell = this.#cells[i];
3172
- if (cell.kind !== "tool" || !isExploreTool(cell.name))
3173
- return false;
3174
- // the run is still growing while NOTHING but explore cells follow —
3175
- // the turn-less noise cells (permission raws, notices) are
3176
- // transparent here for the same reason the run scan sees through
3177
- // them: the streaming execution interleaves them between the calls.
3178
- for (let j = i + 1; j < this.#cells.length; j += 1) {
3179
- const next = this.#cells[j];
3180
- if (next.kind === "raw" || next.kind === "notice")
3181
- continue;
3182
- if (next.kind === "tool" && isExploreTool(next.name))
3183
- continue;
3184
- return false; // a non-explore cell closed the run — commit now
3185
- }
3186
- return true;
3187
- }
2458
+ // It is not only dead weight. Holding done cells in the live
2459
+ // region made the region carry work that was FINISHED, so a burst
2460
+ // of four reads and a shell in flight competed for the same rows
2461
+ // and DC-43's shrink took the running call's output away — the
2462
+ // one thing on the screen the human is waiting for (R7a D). Let
2463
+ // them commit and the room is there.
2464
+ void i;
2465
+ return false;
2466
+ }
2467
+ /* R13 `#growingRun` retired with the rollup's commit hold (#held). */
3188
2468
  /** W14/W13 — the release-time decision at a commit, BEFORE the cell's
3189
2469
  * own render: the folded-turn fold first (a QUIET turn — ended, no
3190
2470
  * text — becomes the ONE fold line; the rest of its thinking/tool
@@ -3193,166 +2473,37 @@ export class Body {
3193
2473
  * the members render [] — the scan is the work order's "group key",
3194
2474
  * derived at commit time, never pre-stored). */
3195
2475
  #foldOrRollup(cell, i, W, ctx) {
3196
- if (cell.kind === "thinking" || cell.kind === "tool") {
3197
- const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
3198
- const seg = this.#segmentOf(i);
3199
- // R3b — the SEGMENT folds, and it folds once.
3200
- //
3201
- // A cell only reaches here once its segment is closed (the hold
3202
- // above keeps an open segment out of the commit loop entirely),
3203
- // so the first cell of a closed segment emits the fold and every
3204
- // cell after it renders nothing. `folded` is the latch: without
3205
- // it the second cell would emit a second fold line for the same
3206
- // work.
3207
- //
3208
- // A SPILLED segment is the honest degradation. The force-commit
3209
- // path does not consult the hold the screen's hard cap wins —
3210
- // so a segment too big for the screen already has rows in the
3211
- // scrollback that cannot be taken back. It renders normally and
3212
- // says nothing false; it simply does not collapse.
3213
- // R3b a ONE-CELL segment does not fold. Collapsing one row into
3214
- // one row gains no space and costs the row's subject: `✦ thought
3215
- // 0s · 1 shell` says strictly less than `shell make build ·
3216
- // exit 0`. The fold exists to stop a screen filling with work
3217
- // rows, and one row is not that.
3218
- // R3d (owner, 2026-08-28) a segment folds only on a QUIET turn.
3219
- //
3220
- // R3b folded every closed segment, and in use that was wrong for
3221
- // a reason the design questions never surfaced: a model narrates
3222
- // between calls, so a turn is not two or three segments, it is
3223
- // one per tool. Every call became its own `✦ thought 2s · 1 read`
3224
- // row the same row count the fold exists to remove, now saying
3225
- // less. The screen is not improved by summarising one thing.
3226
- //
3227
- // The turn's ONE line (renderRecap, R3d) carries the work now,
3228
- // which is where it always belonged: it is already emitted once
3229
- // per turn, in the right place, and it only needed to say what
3230
- // the turn DID rather than "43 tools".
3231
- //
3232
- // The quiet turn keeps its fold because there IS no recap line
3233
- // to carry it: a turn with no text is the fold, and W14's gates
3234
- // pin that shape.
3235
- // DECLARED SUPERSESSION (R3i phase 3, owner-ruled) — THE FOLD IS
3236
- // THE SEGMENT'S, AND TROUBLE DOES NOT STOP IT.
3237
- //
3238
- // ① the SEGMENT. R3d folded the turn; the owner's shape is one
3239
- // summary per stretch of work, standing with the prose that
3240
- // stretch led to. R3b's disease is answered by the two rules
3241
- // below rather than by leaving the segment: a fold must
3242
- // absorb at least TWO rows, and a stretch of exactly one
3243
- // call names its target (see stretchTerms).
3244
- //
3245
- // ② TROUBLE. R3b refused to fold any run holding a failure,
3246
- // and R3g extended that to interrupts. Law 1.3 governs
3247
- // marks versus WORDS and never granted a failure a
3248
- // permanent row; law 1.7 says "Work folds, words do not".
3249
- // So the work folds and the outcome words ride the line —
3250
- // `1 denied: .env` — and the human sees, without pressing
3251
- // anything, that trouble happened, on which call, and what
3252
- // happened. The stderr is behind the key, because it is
3253
- // detail, not outcome. The cost R3b priced as rare measured
3254
- // at 2 failures in 28 calls in the 0.16.7 dogfood, with
3255
- // zero folds as the result.
3256
- if (turn !== undefined && seg !== null && seg.closedAt !== null && !seg.spilled && seg.cells.length >= 2) {
3257
- if (!seg.folded) {
3258
- seg.folded = true;
3259
- seg.headCell = i;
3260
- turn.folded = true;
3261
- // DECLARED SUPERSESSION (R3i phase 3) — A9 NARROWS: the
3262
- // fold carries WORK, never the human's words.
3263
- //
3264
- // A9 put the user's words on the fold as the SGR-7 chip.
3265
- // Measured under R3i, that prints them twice: the chip
3266
- // BAND commits on the frame it is pushed (it always has —
3267
- // `#held` exempts non-thinking/tool cells), so a quiet
3268
- // turn showed ` x ` on its own row and ` x ` again inside
3269
- // the fold directly beneath it. The band is the record of
3270
- // what was asked; this line is the record of what was
3271
- // done. One fact, one row, each.
3272
- return stretchLine({ ...this.#stretchTerms(seg), phase: "settled" }, W);
3273
- }
3274
- return [];
3275
- }
3276
- }
3277
- if (cell.kind !== "tool" || !isExploreTool(cell.name))
3278
- return cellComponent(cell).render(W, ctx);
3279
- // TUI2-R1 (B): the run is over the READ-ONLY SET, not one name —
3280
- // a model exploring mixes read/list/search, and the same-name scan
3281
- // split every real burst into fragments. Writes, edits, shells and
3282
- // extension tools still break the run at the first one.
3283
- // the maximal read-only run around i — forward/backward scans over
3284
- // the cells. The turn-less noise cells (the permission raws, the
3285
- // notices) are TRANSPARENT: the streaming execution (loop.ts launch)
3286
- // interleaves them BETWEEN the calls of one burst, so the run must
3287
- // see through them. It never crosses a user/text/thinking cell —
3288
- // those separate turns and contexts.
3289
- // TUI2-R1.5 ① (VD-1): the backward scan stops at the cells this
3290
- // FRAME is committing. A cell committed in an earlier frame is
3291
- // frozen — its rows are on the screen and in the scrollback — so it
3292
- // can never become the head of a rollup now, and a run that
3293
- // force-committed its first rows mid-turn must not have the rest
3294
- // silently absorbed into a summary that was computed without them.
3295
- // The degraded head keeps its individual row; the rest of the run
3296
- // rolls on its own.
3297
- let s = i;
3298
- let head = i;
3299
- while (s > this.#committedAtFrameStart) {
3300
- const prev = this.#cells[s - 1];
3301
- if (prev.kind === "raw" || prev.kind === "notice") {
3302
- s -= 1;
3303
- continue;
3304
- }
3305
- if (prev.kind !== "tool" || !isExploreTool(prev.name))
3306
- break;
3307
- s -= 1;
3308
- head = s; // a read-only tool precedes — it is the group's head
3309
- }
3310
- let e = i;
3311
- while (e + 1 < this.#cells.length) {
3312
- const next = this.#cells[e + 1];
3313
- if (next.kind === "raw" || next.kind === "notice") {
3314
- e += 1;
3315
- continue;
3316
- }
3317
- if (next.kind !== "tool" || !isExploreTool(next.name))
3318
- break;
3319
- e += 1;
3320
- }
3321
- // the run counts the TOOL cells only — the span's raws are noise.
3322
- const members = this.#cells.slice(s, e + 1).filter((c) => c.kind === "tool");
3323
- if (members.length <= 2)
3324
- return cellComponent(cell).render(W, ctx);
3325
- if (head === i) {
3326
- // the HEAD — the rollup only when EVERY member is done (at the
3327
- // text's release they are — the natural loop commits the run in
3328
- // one frame; the force-commit's early commits degrade to the
3329
- // individual rows, the members render normally after).
3330
- // R3g (2026-08-28): ...and no member is in TROUBLE. A rollup
3331
- // says "explored 3 paths" — a sentence a failed or interrupted
3332
- // call makes false, and the row it replaces was the only place
3333
- // that failure had words. Law 1.3 at the scale of a run: the
3334
- // same rule #segmentHasTrouble applies to the fold. Found when
3335
- // R3g's interrupt-closing made an aborted call `done`, which
3336
- // let a run it never finished roll up as if it had.
3337
- if (!members.every((c) => c.done && !c.isError && c.reason === null))
3338
- return cellComponent(cell).render(W, ctx);
3339
- this.#rolledHeads.add(head);
3340
- let total = 0;
3341
- const targets = [];
3342
- // TUI2-R1 (B): the per-tool parts, in first-call order — the
3343
- // exploration row's counts and its expanded list both read them.
3344
- // A search's subject is the PATTERN it looked for (quoted); a
3345
- // read's or a list's is the path it named.
3346
- cell.rolled = rolledOf(members);
3347
- return cellComponent(cell).render(W, ctx);
3348
- }
3349
- // a MEMBER of an already-rolled run → [] (its rows live in the
3350
- // head's summary). A member of a run whose head committed
3351
- // INDIVIDUALLY (the force-commit's degradation) renders normally —
3352
- // the head is not in #rolledHeads, the run never rolls after the
3353
- // head's individual commit.
3354
- if (this.#rolledHeads.has(head))
3355
- return [];
2476
+ // R13 DECLARED REVERSAL, three at once, all of them the same
2477
+ // idea and all of them owner-ruled on 2026-09-03:
2478
+ //
2479
+ // · the SEGMENT FOLD (R3b–R3i, W14) a closed stretch of two or
2480
+ // more cells collapsed into one settled line;
2481
+ // · the W13 ROLLUP and the `rolled` cell field a run of three
2482
+ // or more same-tool calls collapsed into a group summary;
2483
+ // · TUI2-R1 (B)'s EXPLORATION ROW the mixed read-only variant
2484
+ // of the same collapse.
2485
+ //
2486
+ // Every one of them answered the same pressure: ungrounded output
2487
+ // rows owned the screen, so work was collapsed into sentences
2488
+ // ABOUT the work. The card is what changes that arithmetic — a
2489
+ // call's rows sit inside a surface that says where it begins and
2490
+ // ends, so five of them read as one object rather than five loose
2491
+ // lines and the owner's ruling is that the collapse costs more
2492
+ // than it buys: a page where the machine's work is sometimes a
2493
+ // card, sometimes a summary line and sometimes nothing at all is
2494
+ // a page a reader cannot predict. One rhythm, one surface.
2495
+ //
2496
+ // So every cell renders itself, and this method is the record of
2497
+ // what used to happen here. What the folds bought is bought
2498
+ // differently now: the preview cap (five rows a call, E1's read
2499
+ // showing none) is what keeps a burst from owning the screen, and
2500
+ // it is a CONSTANT per call rather than a decision about runs.
2501
+ //
2502
+ // What goes with them: `#growingRun`'s commit hold existed only so
2503
+ // a run's committed FORM could be decided once the run closed
2504
+ // with no run-level form left to decide, a done cell commits when
2505
+ // it is done. `#rolledHeads`, `rolledOf`, `rolledTitle`,
2506
+ // `rolledDetail` and the `rolled` field go with the rollup.
3356
2507
  return cellComponent(cell).render(W, ctx);
3357
2508
  }
3358
2509
  /** The slot occupant's extra rows — the slash-command menu (above the
@@ -3675,9 +2826,26 @@ export class Body {
3675
2826
  // answer lands and the eye is on it, is the cheaper of the two.
3676
2827
  // The A8b guard is written against a real session; this is what
3677
2828
  // it exists to catch.
3678
- const skip = overlay
3679
- ? this.#lastSkip
3680
- : Math.max(0, all.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
2829
+ const fresh = Math.max(0, all.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
2830
+ // R13 — THE WINDOW'S TOP NEVER FALLS. `fresh` is read off the
2831
+ // CURRENT model height, so it drops the moment the live region
2832
+ // shrinks — and rows [0, #scrolledOff) are already in the
2833
+ // terminal's scrollback, immutable (ADR-0046). Painting them again
2834
+ // is a window that un-scrolls, which a terminal cannot do: the
2835
+ // rows come back on screen while their originals stay in the
2836
+ // history above, and the transcript gains a duplicate.
2837
+ //
2838
+ // R4's standing slot made the live region monotone WITHIN a turn,
2839
+ // so this could not arise and the clamp was never needed. R13
2840
+ // retires the slot — a card shrinks when its call settles (E2) —
2841
+ // which is what put the fall on the table. The clamp is the same
2842
+ // rule `#emitScroll`'s floor already keeps for the scroll; it now
2843
+ // also governs the paint.
2844
+ //
2845
+ // A RESIZE is exempt, and must be: the terminal reflows and
2846
+ // scrolls on its own before we are called, so the fresh count is
2847
+ // the truth there and a high-water mark is DC-34's own defect.
2848
+ const skip = overlay ? this.#lastSkip : this.#resizeFrame ? fresh : Math.max(this.#scrolledOff, fresh);
3681
2849
  // A8b (the shrink-trigger's completion): the rows that LEAVE the
3682
2850
  // window scroll into the terminal's scrollback — the LF mechanism
3683
2851
  // (the steady path's own). Only the rows the paint re-covers (the
@@ -3726,8 +2894,26 @@ export class Body {
3726
2894
  this.#resizeFrame = false;
3727
2895
  }
3728
2896
  else if (!overlay) {
3729
- const floor = Math.max(0, this.#committedLines + CHROME_ROWS - H);
3730
- this.#emitScroll(out, W, H, all, floor);
2897
+ // DC-46 THE ROWS THAT LEAVE THE WINDOW SCROLL, and the target
2898
+ // is `skip` itself.
2899
+ //
2900
+ // It used to be a FLOOR: where the window's top would sit with
2901
+ // the live band empty. The floor was the conservative choice
2902
+ // because the top could come back down when the live region
2903
+ // shrank, and a scroll is the frame's one irreversible act — so
2904
+ // only the part of the movement that could never reverse was
2905
+ // paid out.
2906
+ //
2907
+ // R13 makes the top monotone (the clamp above: rows in
2908
+ // [0, #scrolledOff) are in the terminal's scrollback and the
2909
+ // paint may not go back above them), so a row above `skip` has
2910
+ // left for good and there is nothing conservative left to be.
2911
+ // Keeping the floor with a live region that can now grow by a
2912
+ // whole card LOST rows: painted over in place, never scrolled,
2913
+ // present in neither the screen nor the scrollback. Measured on
2914
+ // R7a D — a four-call burst on a 24-row terminal, and the first
2915
+ // call's head row was never emitted at all.
2916
+ this.#emitScroll(out, W, H, all, skip);
3731
2917
  }
3732
2918
  if (!overlay)
3733
2919
  this.#lastSkip = skip;