@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.
- package/dist/compositor.js +175 -989
- package/dist/editor.d.ts +1 -0
- package/dist/editor.js +24 -0
- package/dist/transcript.js +1 -1
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
84
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
-
|
|
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,
|
|
1767
|
+
const rows = this.#project(W, ctx, CAP_PREVIEW);
|
|
2004
1768
|
if (cap === undefined || rows.length <= cap)
|
|
2005
1769
|
return rows;
|
|
2006
|
-
//
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2809
|
-
*
|
|
2810
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
2853
|
-
//
|
|
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
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
//
|
|
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
|
-
//
|
|
3114
|
-
//
|
|
3115
|
-
//
|
|
3116
|
-
//
|
|
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
|
-
//
|
|
3119
|
-
//
|
|
3120
|
-
//
|
|
3121
|
-
//
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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
|
|
3679
|
-
|
|
3680
|
-
|
|
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
|
-
|
|
3730
|
-
|
|
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;
|