@vincemakes/kiso-tui 0.24.5 → 0.26.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/ask-panel.js +1 -1
- package/dist/at-picker.js +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +1 -1
- package/dist/compositor.d.ts +40 -67
- package/dist/compositor.js +433 -1043
- package/dist/context-ledger.js +1 -1
- package/dist/editor.d.ts +6 -7
- package/dist/editor.js +376 -786
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{render.js → lines.js} +3 -3
- package/dist/panel-input.d.ts +105 -0
- package/dist/panel-input.js +468 -0
- package/dist/pick-input.d.ts +34 -0
- package/dist/pick-input.js +100 -0
- package/dist/session-picker.js +1 -1
- package/dist/status.js +1 -1
- package/dist/transcript.js +4 -27
- package/package.json +2 -2
- /package/dist/{render.d.ts → lines.d.ts} +0 -0
package/dist/compositor.js
CHANGED
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
* the reference implementation's model): a line COMMITS (leaves the
|
|
12
12
|
* live region) via the real-LF
|
|
13
13
|
* scroll at the last row (`\x1b[1B\n` — CUP-free) when its cell is
|
|
14
|
-
* DONE and the region needs the room.
|
|
15
|
-
* re-emitted — the native scrollback gets them
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* DONE and the region needs the room. Within a geometry, committed
|
|
15
|
+
* bytes are never re-emitted — the native scrollback gets them. The
|
|
16
|
+
* one exception is a SETTLED RESIZE (ADR-0046 Amendment 1, R14):
|
|
17
|
+
* `2J H 3J`, then the whole session reprinted from the model, so the
|
|
18
|
+
* terminal holds exactly one rendering; the shell history the
|
|
19
|
+
* terminal held before kiso started is the declared cost;
|
|
18
20
|
* - the live region (content + chrome + menu) is hard-capped: the
|
|
19
21
|
* content at H−4 (V6-3 — the four-row chrome); overflow FORCE-
|
|
20
|
-
* commits the oldest live
|
|
21
|
-
*
|
|
22
|
+
* commits the oldest live cell that has a committed form — never a
|
|
23
|
+
* tool call still running (DC-53); a running card's window shrinks
|
|
24
|
+
* to the room instead (DC-43);
|
|
22
25
|
* - two crash invariants: ① every emitted line's visible width ≤ W
|
|
23
26
|
* (components fold; a violation THROWS with diagnostics — the
|
|
24
27
|
* no-silent-truncate ruling); ② every steady-
|
|
@@ -47,13 +50,13 @@ import { MENU_ITEMS, displayWidth } from "./editor.js";
|
|
|
47
50
|
import { leadWidth } from "./width.js"; // W23: the ONE width authority (the editor, #inputRow, and editCol share it)
|
|
48
51
|
// KC3.5: the panel-slot reads come from the DISPATCHERS — one source
|
|
49
52
|
// for four reads, so an ask can never render half as an approval.
|
|
50
|
-
import { panelFrameOf, panelLeadOf,
|
|
53
|
+
import { panelFrameOf, panelLeadOf, panelStatusOf } from "./ask-panel.js";
|
|
51
54
|
import { MOUSE_OFF } from "./editor.js";
|
|
52
55
|
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
53
56
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
54
57
|
import { sessionPickerRows } from "./session-picker.js";
|
|
55
|
-
import { CAP_PREVIEW, thinkingRow,
|
|
56
|
-
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary
|
|
58
|
+
import { CAP_PREVIEW, thinkingRow, Container, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, gutterCut, cutLine, pendingQueueRows, statusLine, visibleWidth, breathFrame, } from "./components.js";
|
|
59
|
+
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary } from "./lines.js";
|
|
57
60
|
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
58
61
|
// R5 — the transcript viewer's PURE projection. The compositor supplies
|
|
59
62
|
// the entries (it holds the cells); the arrangement lives there.
|
|
@@ -80,50 +83,12 @@ const LIVE_ACT_HEADS = 3;
|
|
|
80
83
|
/** R8 — the command band's window: five rows plus a counter, the same
|
|
81
84
|
* budget the composer's own ceiling can afford above it. */
|
|
82
85
|
const MENU_WINDOW = 5;
|
|
83
|
-
/* R13 — `rolledTitle`, `rolledDetail` and `rolledOf` retired with the
|
|
84
|
-
W13 rollup and TUI2-R1 (B)'s exploration row (see #foldOrRollup). */
|
|
85
|
-
/** R3b — the turn's open segment, opened on demand at the first cell of
|
|
86
|
-
* work that follows a text block (or the turn's start). Returns null
|
|
87
|
-
* only when there is no turn at all, which is the pipe path's shape. */
|
|
88
|
-
function openSegment(turn, now) {
|
|
89
|
-
if (turn === undefined)
|
|
90
|
-
return null;
|
|
91
|
-
const last = turn.segments[turn.segments.length - 1];
|
|
92
|
-
if (last !== undefined && last.closedAt === null)
|
|
93
|
-
return last;
|
|
94
|
-
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), thinkingMs: 0, thinkingSince: null, folded: false, spilled: false, headCell: null, cells: [] };
|
|
95
|
-
turn.segments.push(fresh);
|
|
96
|
-
return fresh;
|
|
97
|
-
}
|
|
98
|
-
/** R3b — close the turn's open segment, if it has one. Idempotent: text
|
|
99
|
-
* arriving twice in a row closes nothing the second time, which is what
|
|
100
|
-
* keeps a zero-cell segment from ever existing. */
|
|
101
|
-
function closeSegment(turn, now) {
|
|
102
|
-
const last = turn?.segments[turn.segments.length - 1];
|
|
103
|
-
if (last === undefined || last.closedAt !== null)
|
|
104
|
-
return;
|
|
105
|
-
stopThinking(last, now);
|
|
106
|
-
last.closedAt = now;
|
|
107
|
-
}
|
|
108
|
-
/** R3i — the segment's thinking clock stops. It runs from the first
|
|
109
|
-
* thinking delta of a stretch and stops at the first NON-thinking
|
|
110
|
-
* event, the same rule the CLI applies to the turn — so `thought Ns`
|
|
111
|
-
* is thinking time at every scale and never a wall clock wearing the
|
|
112
|
-
* word (the R3g defect, kept closed at the new scale). */
|
|
113
|
-
function stopThinking(seg, now) {
|
|
114
|
-
if (seg === undefined || seg.thinkingSince === null)
|
|
115
|
-
return;
|
|
116
|
-
seg.thinkingMs += Math.max(0, now - seg.thinkingSince);
|
|
117
|
-
seg.thinkingSince = null;
|
|
118
|
-
}
|
|
119
86
|
/** W20 — the whole-table-replace comparison: the live task block only
|
|
120
87
|
* redraws when the items actually changed (the task extension's
|
|
121
88
|
* idempotent shape — an unchanged replace is a no-op, no frame). */
|
|
122
89
|
function sameTask(a, b) {
|
|
123
90
|
return a.length === b.length && a.every((x, i) => x.text === b[i].text && x.status === b[i].status);
|
|
124
91
|
}
|
|
125
|
-
/** The one compositor — implements the Body façade AND the Dock chrome
|
|
126
|
-
* API (see the class comments on each method group). */
|
|
127
92
|
export class Body {
|
|
128
93
|
#opts;
|
|
129
94
|
#cells = [];
|
|
@@ -198,20 +163,28 @@ export class Body {
|
|
|
198
163
|
#needsReset = false;
|
|
199
164
|
#resizeTimer = null;
|
|
200
165
|
#lastH = 0;
|
|
201
|
-
/**
|
|
166
|
+
/** R14 — the geometry the last frame PAINTED, and the snapshot of it
|
|
167
|
+
* taken when a winch opens a settle window.
|
|
202
168
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
*
|
|
214
|
-
|
|
169
|
+
* Three versions of this question were wrong before this one.
|
|
170
|
+
* Seeding in `enter()` never ran under the unit pool (it bails on a
|
|
171
|
+
* non-TTY), so a same-size winch erased the scrollback. Comparing
|
|
172
|
+
* against the CURRENT frame's geometry let an ordinary frame render
|
|
173
|
+
* at the new size between the winch and the settle and eat the
|
|
174
|
+
* change, so a real resize silently stopped reprinting. Seeding once
|
|
175
|
+
* on the first frame had the same hole whenever the first frame had
|
|
176
|
+
* not run yet.
|
|
177
|
+
*
|
|
178
|
+
* The question the settle actually asks is "is the terminal holding
|
|
179
|
+
* a rendering made at a DIFFERENT geometry than the one we have
|
|
180
|
+
* now" — so the comparison is against what was on screen when the
|
|
181
|
+
* winch arrived, snapshotted then, once per storm. */
|
|
182
|
+
#paintedW = 0;
|
|
183
|
+
#paintedH = 0;
|
|
184
|
+
#winchFromW = 0;
|
|
185
|
+
#winchFromH = 0;
|
|
186
|
+
/** DC-50 — the ONE expansion switch ctrl+o flips. */
|
|
187
|
+
#expandedAll = false;
|
|
215
188
|
// KC1 §6: the composer's recorded extent — the row count the last
|
|
216
189
|
// frame drew (exit's clear walks it) and the row its CHA parked the
|
|
217
190
|
// cursor on (the steady frame's relative anchor). N = 1 reproduces
|
|
@@ -224,11 +197,6 @@ export class Body {
|
|
|
224
197
|
#lastThinking = null;
|
|
225
198
|
#lastTool = null;
|
|
226
199
|
#pendingCalls = new Map();
|
|
227
|
-
/** R3b — cell index → the index of the segment it belongs to, for
|
|
228
|
-
* thinking/tool cells; -1 for every other kind. Parallel to #cells,
|
|
229
|
-
* because a segment is the COMPOSITOR's bookkeeping and does not
|
|
230
|
-
* belong on the cell type the renderer sees. */
|
|
231
|
-
#cellSegment = [];
|
|
232
200
|
#pipeBuf = ""; // the passthrough's thinking buffer
|
|
233
201
|
/** TUI2-MD ⑤ — the markdown scanner of the message currently
|
|
234
202
|
* streaming, and the cell index its first block landed at. Null
|
|
@@ -237,26 +205,15 @@ export class Body {
|
|
|
237
205
|
#mdBase = 0;
|
|
238
206
|
#toolCells = new Map(); // callId → cell index (parallel tools)
|
|
239
207
|
// W15: the collapsed (cut) tool cells — committed cells whose last
|
|
240
|
-
// rendered row carried the "ctrl+o" affordance
|
|
241
|
-
//
|
|
208
|
+
// rendered row carried the "ctrl+o" affordance.
|
|
209
|
+
//
|
|
210
|
+
// DC-50 / R14: the expand key no longer WALKS this. It is the
|
|
211
|
+
// VIEWER's index now (ctrl+r, `#viewerEntries`) — the two surfaces
|
|
212
|
+
// coexist and §9 says so. It is kept, not retired, for that reader.
|
|
242
213
|
#collapsed = [];
|
|
243
|
-
|
|
244
|
-
* `#collapsed` is unshifted on every commit that carries the key, so
|
|
245
|
-
* a numeric pointer's target silently CHANGED whenever a new fold
|
|
246
|
-
* landed mid-cycle: the ring was not stable under itself, and the
|
|
247
|
-
* next press opened something other than what the last press
|
|
248
|
-
* implied. This set records what the current cycle has already
|
|
249
|
-
* opened; the walk takes the newest entry not in it, and empties it
|
|
250
|
-
* when every entry has been seen. */
|
|
251
|
-
#opened = new Set();
|
|
252
|
-
// W14: the turn records — one per userLine, the fold-hold's state
|
|
253
|
-
// machine (ended / hasText / folded) plus the folded-turn line's
|
|
254
|
-
// counts (accumulated at toolStart). The cells carry the record's
|
|
214
|
+
// the turn records — one per userLine; the cells carry the record's
|
|
255
215
|
// index as their turn boundary.
|
|
256
216
|
#turns = [];
|
|
257
|
-
// W13: the rolled-up run heads — the commit-time scan's verdict:
|
|
258
|
-
// the head's group summary renders, the members render [].
|
|
259
|
-
#rolledHeads = new Set();
|
|
260
217
|
#write;
|
|
261
218
|
#resizeHandler = null;
|
|
262
219
|
/** TUI2-R3v2 ②: the panel option rows' absolute screen span, as of the
|
|
@@ -396,10 +353,8 @@ export class Body {
|
|
|
396
353
|
}
|
|
397
354
|
this.#closeOpenThinking();
|
|
398
355
|
this.#closeOpenText();
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
// the user's own words ride the record — the fold's leading chip.
|
|
402
|
-
this.#turns.push({ ended: false, hasText: false, begun: false, thoughtSeconds: 0, reads: 0, edits: 0, others: new Map(), seen: new Map(), words: text, folded: false, segments: [] });
|
|
356
|
+
// the turn boundary; the cell carries the record's index.
|
|
357
|
+
this.#turns.push({ ended: false, hasText: false, begun: false, thoughtSeconds: 0 });
|
|
403
358
|
this.#cells.push({ kind: "user", text, done: true, turn: this.#turns.length - 1 });
|
|
404
359
|
this.#mark();
|
|
405
360
|
}
|
|
@@ -414,42 +369,15 @@ export class Body {
|
|
|
414
369
|
}
|
|
415
370
|
else {
|
|
416
371
|
this.#cells.push({ kind: "thinking", text, done: false, turn: this.#turns.length - 1 });
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// R3b made thinking open a segment, on the reading that it is
|
|
421
|
-
// work like a tool call. Four rounds of consequences followed
|
|
422
|
-
// from that one classification: folded away with the calls, it
|
|
423
|
-
// became unreachable, and R4's printed ordinal, R5's viewer,
|
|
424
|
-
// R6's subject index and a look-back viewport were each built
|
|
425
|
-
// to hand it back. The owner's ruling is to stop hiding it —
|
|
426
|
-
// and then none of those mechanisms is answering a question
|
|
427
|
-
// anyone still asks.
|
|
428
|
-
//
|
|
429
|
-
// So thinking CLOSES the open segment, exactly as text does
|
|
430
|
-
// (see textAppend): a segment is what sits between two of
|
|
431
|
-
// these. It must close rather than merely not-open, because
|
|
432
|
-
// `#committed` is a PREFIX count — a thinking cell cannot
|
|
433
|
-
// commit past a held call, so think → call → think would
|
|
434
|
-
// otherwise flush at the segment's close with the second
|
|
435
|
-
// thought printing BELOW the fold that contains the later
|
|
436
|
-
// call.
|
|
437
|
-
//
|
|
438
|
-
// Consequence, and it is wanted: the segment's thinking clock
|
|
439
|
-
// never starts, so `thought Ns` drops off every fold line by
|
|
440
|
-
// R3h's own zero-term rule. The line stops claiming a fact the
|
|
441
|
-
// paragraph above it already states in full.
|
|
372
|
+
// R7 (owner-ruled 2026-08-31): thinking is WORDS, not work — a
|
|
373
|
+
// cell like prose.
|
|
442
374
|
const t0 = this.#turns[this.#turns.length - 1];
|
|
443
|
-
closeSegment(t0, Date.now());
|
|
444
375
|
if (t0 !== undefined)
|
|
445
|
-
t0.begun = true;
|
|
446
|
-
// R3i:
|
|
447
|
-
// twinkles", and `#armSpinner`'s
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
// never moved at all. The line's seconds are a frame-time
|
|
451
|
-
// derivation, so without the beat they also never ticked: the
|
|
452
|
-
// row read `thinking 0s` for as long as the model thought.
|
|
376
|
+
t0.begun = true;
|
|
377
|
+
// R3i: the beat starts HERE. Law 1.4 says "a running thought
|
|
378
|
+
// twinkles", and `#armSpinner`'s predicate includes an open
|
|
379
|
+
// thinking cell — but its only caller used to be `toolRunning`,
|
|
380
|
+
// so a turn that thought and did nothing else never moved.
|
|
453
381
|
this.#armSpinner();
|
|
454
382
|
}
|
|
455
383
|
this.#mark();
|
|
@@ -457,10 +385,6 @@ export class Body {
|
|
|
457
385
|
thinkingEnd() {
|
|
458
386
|
const last = this.#cells[this.#cells.length - 1];
|
|
459
387
|
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
460
|
-
// R3i: every closer — text, a notice, a terminal label, the next
|
|
461
|
-
// turn — routes through here, so the clock cannot keep running
|
|
462
|
-
// past the thing that ended it.
|
|
463
|
-
stopThinking(this.#turns[this.#turns.length - 1]?.segments.at(-1), Date.now());
|
|
464
388
|
last.done = true;
|
|
465
389
|
this.#lastThinking = last.text;
|
|
466
390
|
if (!this.#isActive())
|
|
@@ -485,9 +409,7 @@ export class Body {
|
|
|
485
409
|
// `done` flag says otherwise. The commit loop takes leading DONE
|
|
486
410
|
// cells, so that one stale flag parked the whole rest of the turn
|
|
487
411
|
// behind it — every tool cell then reached the screen through the
|
|
488
|
-
// FORCE-commit path
|
|
489
|
-
// why the walkthrough saw nine individual rows: not a fold that
|
|
490
|
-
// declined to form, a fold that was never consulted.
|
|
412
|
+
// FORCE-commit path.
|
|
491
413
|
this.#closeOpenThinking();
|
|
492
414
|
this.#closeOpenText();
|
|
493
415
|
// W12: the cell carries the delegate's child roles from the FULL
|
|
@@ -502,73 +424,11 @@ export class Body {
|
|
|
502
424
|
}
|
|
503
425
|
this.#toolCells.set(callId, this.#cells.length);
|
|
504
426
|
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 });
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
// order). The CLI's recap counts the same way (edit_file).
|
|
427
|
+
// R3i phase 5 — an ANSWER is words (law 1.7): `ask_user` never
|
|
428
|
+
// counted as the turn's work, so it does not mark the turn begun.
|
|
508
429
|
const turn = this.#turns[this.#turns.length - 1];
|
|
509
|
-
if (turn !== undefined)
|
|
510
|
-
|
|
511
|
-
// distinct thing, not the act. Reading one file twice used to
|
|
512
|
-
// fold as `read 2 files` — a sentence law 1.3 forbids, and one
|
|
513
|
-
// this product shipped. `bump` is false on the second sighting
|
|
514
|
-
// of a target the term has already counted; an ACT-counting
|
|
515
|
-
// tool (a search, a shell command) always bumps, because two
|
|
516
|
-
// searches for the same pattern really are two searches.
|
|
517
|
-
// R3i phase 5 — an ANSWER is words, and words do not fold (law
|
|
518
|
-
// 1.7). `ask_user` closes the open stretch exactly as prose
|
|
519
|
-
// does, and never joins one: absorbed into `1 × ask_user`,
|
|
520
|
-
// what the human said would be gone from the screen — and the
|
|
521
|
-
// one thing a summary must not do is speak for the human.
|
|
522
|
-
if (name === "ask_user") {
|
|
523
|
-
// no stamp: it belongs to NO stretch, so no fold can speak
|
|
524
|
-
// for it — the same standing a block of prose has.
|
|
525
|
-
closeSegment(turn, Date.now());
|
|
526
|
-
this.#mark();
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
530
|
-
const bump = (rec) => {
|
|
531
|
-
if (target === null)
|
|
532
|
-
return true;
|
|
533
|
-
let set = rec.seen.get(name);
|
|
534
|
-
if (set === undefined) {
|
|
535
|
-
set = new Set();
|
|
536
|
-
rec.seen.set(name, set);
|
|
537
|
-
}
|
|
538
|
-
if (set.has(target))
|
|
539
|
-
return false;
|
|
540
|
-
set.add(target);
|
|
541
|
-
return true;
|
|
542
|
-
};
|
|
543
|
-
if (bump(turn)) {
|
|
544
|
-
if (name === "read_file")
|
|
545
|
-
turn.reads += 1;
|
|
546
|
-
else if (name === "edit_file")
|
|
547
|
-
turn.edits += 1;
|
|
548
|
-
else
|
|
549
|
-
turn.others.set(name, (turn.others.get(name) ?? 0) + 1);
|
|
550
|
-
}
|
|
551
|
-
// R3b: and into the SEGMENT, which opens here when this is the
|
|
552
|
-
// first work since the last text block. Its set is its OWN — a
|
|
553
|
-
// file read once per segment is one file in each segment's
|
|
554
|
-
// terms and one file in the turn's.
|
|
555
|
-
const seg = openSegment(turn, Date.now());
|
|
556
|
-
// R3i: a tool call is a NON-thinking event — the clock stops,
|
|
557
|
-
// exactly as the CLI's does at the same boundary.
|
|
558
|
-
stopThinking(seg ?? undefined, Date.now());
|
|
559
|
-
if (seg !== null && bump(seg)) {
|
|
560
|
-
if (name === "read_file")
|
|
561
|
-
seg.reads += 1;
|
|
562
|
-
else if (name === "edit_file")
|
|
563
|
-
seg.edits += 1;
|
|
564
|
-
else
|
|
565
|
-
seg.others.set(name, (seg.others.get(name) ?? 0) + 1);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
this.#stampSegment();
|
|
569
|
-
const t1 = this.#turns[this.#turns.length - 1];
|
|
570
|
-
if (t1 !== undefined)
|
|
571
|
-
t1.begun = true; // R6/D1: the block allocates here
|
|
430
|
+
if (turn !== undefined && name !== "ask_user")
|
|
431
|
+
turn.begun = true;
|
|
572
432
|
this.#mark();
|
|
573
433
|
}
|
|
574
434
|
toolApproval(callId, diff) {
|
|
@@ -684,17 +544,9 @@ export class Body {
|
|
|
684
544
|
this.#write(escapeTerminal(text));
|
|
685
545
|
return;
|
|
686
546
|
}
|
|
687
|
-
// W14: the text's arrival RELEASES the fold-hold — the turn now
|
|
688
|
-
// has text, its held cells commit individually (with the W13
|
|
689
|
-
// rollups; the fold is only for the QUIET turn).
|
|
690
547
|
const turn = this.#turns[this.#turns.length - 1];
|
|
691
548
|
if (turn !== undefined)
|
|
692
549
|
turn.hasText = true;
|
|
693
|
-
// R3b: text CLOSES the open segment. This is the boundary design.md
|
|
694
|
-
// §8 names — "folding at every text boundary changes what commits
|
|
695
|
-
// and when" — and it is the whole mechanism: a segment is what sits
|
|
696
|
-
// between two of these.
|
|
697
|
-
closeSegment(turn, Date.now());
|
|
698
550
|
// TUI2-MD ⑤: assistant body text is MARKDOWN, scanned as it
|
|
699
551
|
// streams. The scanner yields CLOSED blocks (final source, final
|
|
700
552
|
// render) and one OPEN tail block; each becomes a cell, and the
|
|
@@ -762,14 +614,9 @@ export class Body {
|
|
|
762
614
|
this.#endMd();
|
|
763
615
|
this.#mark();
|
|
764
616
|
}
|
|
765
|
-
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* the CLI's wall-clocked thinking window. The QUIET turn (ended, no
|
|
769
|
-
* text) releases its held cells as the ONE fold line; a turn with
|
|
770
|
-
* text releases them as individual commits (the W13 rollups). The
|
|
771
|
-
* release is LAZY — the held cells commit at the next frame, when
|
|
772
|
-
* the fold/rollup decision runs. */
|
|
617
|
+
/** The turn boundary's END: the CLI calls this at the run's terminal
|
|
618
|
+
* event, once per run, BEFORE the recap. `thoughtSeconds` is the
|
|
619
|
+
* CLI's wall-clocked thinking window. */
|
|
773
620
|
endTurn(thoughtSeconds) {
|
|
774
621
|
if (!this.#isActive())
|
|
775
622
|
return;
|
|
@@ -778,9 +625,6 @@ export class Body {
|
|
|
778
625
|
return;
|
|
779
626
|
turn.ended = true;
|
|
780
627
|
turn.thoughtSeconds = thoughtSeconds;
|
|
781
|
-
// R3b: the settle closes the last open segment — the turn's end is
|
|
782
|
-
// a boundary exactly as a text block is.
|
|
783
|
-
closeSegment(turn, Date.now());
|
|
784
628
|
// W20: the turn's live task block settles HERE — the ONE recap
|
|
785
629
|
// block for the turn ("`task done · N items · <duration>", the
|
|
786
630
|
// duration clocked compositor-side from the block's first call —
|
|
@@ -803,9 +647,7 @@ export class Body {
|
|
|
803
647
|
// and only ever left it through the force-commit cap. The turn's
|
|
804
648
|
// end is the boundary that closes them, exactly as it closes an
|
|
805
649
|
// open thinking cell. `reason` is set so the row keeps its words
|
|
806
|
-
//
|
|
807
|
-
// interruption is trouble, and law 1.3 says trouble is never
|
|
808
|
-
// summarised away.
|
|
650
|
+
// (law 1.3: trouble is never summarised away).
|
|
809
651
|
for (const c of this.#cells) {
|
|
810
652
|
if (c.kind === "tool" && !c.done) {
|
|
811
653
|
c.state = "done";
|
|
@@ -815,10 +657,8 @@ export class Body {
|
|
|
815
657
|
}
|
|
816
658
|
}
|
|
817
659
|
// the QUIET turn: an open thinking cell closes at the boundary —
|
|
818
|
-
// its natural closer is the text's arrival
|
|
819
|
-
//
|
|
820
|
-
// it (the commit loop only takes done cells — the fold would stall
|
|
821
|
-
// forever behind the live thinking).
|
|
660
|
+
// its natural closer is the text's arrival, which a text-less turn
|
|
661
|
+
// never has, and the commit loop only takes done cells.
|
|
822
662
|
for (let i = this.#cells.length - 1; i >= 0; i -= 1) {
|
|
823
663
|
const c = this.#cells[i];
|
|
824
664
|
if (c.kind === "thinking" && !c.done) {
|
|
@@ -946,68 +786,24 @@ export class Body {
|
|
|
946
786
|
lastTool() {
|
|
947
787
|
return this.#lastTool;
|
|
948
788
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
* block at the bottom instead, the /last idiom aimed at a chosen
|
|
955
|
-
* cell: the pointer cycles the collapsed history, newest first, and
|
|
956
|
-
* the header names the target ("N turns back" — the user cells
|
|
957
|
-
* after it), so every press tells the user what they got. */
|
|
789
|
+
/* DECLARED REVERSAL (D-S2-1, owner-ruled 2026-09-06): `#focusIndex`
|
|
790
|
+
stood here — TUI2-R2 ⑤'s bright ctrl+o token on the newest live
|
|
791
|
+
card, "the cell the next press will act on". DC-50 made ctrl+o a
|
|
792
|
+
global switch, so there was no target left for a marker to name;
|
|
793
|
+
the status row's idle hint names the switch instead (#statusSource). */
|
|
958
794
|
/**
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
* The committed fallback (the #collapsed ring) is deliberately NOT
|
|
968
|
-
* marked: those rows are frozen history, never re-emitted, so a tint
|
|
969
|
-
* on them could not be moved when the pointer advances. A live target
|
|
970
|
-
* is the one the marker can tell the truth about.
|
|
971
|
-
*/
|
|
972
|
-
#focusIndex() {
|
|
973
|
-
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
974
|
-
const cell = this.#cells[i];
|
|
975
|
-
if (cell.kind === "tool" && cell.state !== "pending")
|
|
976
|
-
return i;
|
|
977
|
-
if (cell.kind === "checklist" && !cell.done)
|
|
978
|
-
return i;
|
|
979
|
-
}
|
|
980
|
-
return -1;
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* R4 (C4d) — THE APPEND-ONLY RE-WRAP.
|
|
984
|
-
*
|
|
985
|
-
* The owner's report: resize the window and the reference
|
|
986
|
-
* implementation's text re-wraps to the new width while kiso's does
|
|
987
|
-
* not. It is true, and it is not a bug to be fixed — it is the price
|
|
988
|
-
* of ADR-0046, and the price is worth naming precisely.
|
|
989
|
-
*
|
|
990
|
-
* A terminal can only reflow a SOFT-wrapped line: one long logical
|
|
991
|
-
* line the terminal itself wrapped as the cursor flowed past the last
|
|
992
|
-
* column. Every row kiso commits is either painted by cursor
|
|
993
|
-
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
994
|
-
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
995
|
-
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
996
|
-
* application hard-split. That same LF is what makes the transcript
|
|
997
|
-
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
998
|
-
* product whose transcript reflows is a product that repaints its
|
|
999
|
-
* transcript from its own memory, and that transcript dies with it.
|
|
1000
|
-
*
|
|
1001
|
-
* What kiso can do — and this is all it can do — is APPEND. The
|
|
1002
|
-
* committed cells are still in memory; re-render them at the current
|
|
1003
|
-
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
1004
|
-
* above is rewritten, so ADR-0046 holds exactly.
|
|
795
|
+
* R4 (C4d) — `/rewrap`: the recent PROSE, re-rendered at the current
|
|
796
|
+
* width and APPENDED at the bottom. Committed rows carry no soft-wrap
|
|
797
|
+
* flag (autowrap is OFF; every row is painted or scrolled out by a
|
|
798
|
+
* bare LF), so the terminal cannot reflow them itself. R14
|
|
799
|
+
* (ADR-0046 Amendment 1, D-B3) made a settled resize reprint the
|
|
800
|
+
* whole session from the model on its own; this stays as the
|
|
801
|
+
* user-invoked form, and it still appends rather than erasing —
|
|
802
|
+
* nothing above the append is rewritten.
|
|
1005
803
|
*
|
|
1006
804
|
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
1007
805
|
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
1008
|
-
*
|
|
1009
|
-
* width ladders, and re-printing them would duplicate work the folds
|
|
1010
|
-
* exist to state once.
|
|
806
|
+
* Cards and chips are short and carry their own width ladders.
|
|
1011
807
|
*/
|
|
1012
808
|
rewrap() {
|
|
1013
809
|
const W = this.#opts.width();
|
|
@@ -1134,8 +930,7 @@ export class Body {
|
|
|
1134
930
|
continue;
|
|
1135
931
|
// the tool card's FULL body — the same rows its own ctrl+o
|
|
1136
932
|
// opens. The expanded flag is saved and restored inside this
|
|
1137
|
-
// synchronous call
|
|
1138
|
-
// used for head.rolled; it never outlives the render, so the
|
|
933
|
+
// synchronous call; it never outlives the render, so the
|
|
1139
934
|
// committed geometry #committedLines derives can never see it.
|
|
1140
935
|
const saved = cell.expanded;
|
|
1141
936
|
cell.expanded = true;
|
|
@@ -1167,112 +962,13 @@ export class Body {
|
|
|
1167
962
|
cutLine(`${p.dim} ${viewerHint(this.#viewer, entries)}${p.reset}`, W),
|
|
1168
963
|
];
|
|
1169
964
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
* The ring walks newest-back and restarts its cycle once every entry
|
|
1178
|
-
* has been opened (R4/C1, which is what makes the walk immune to the
|
|
1179
|
-
* ring growing underneath it). With a ring of ONE the restart is
|
|
1180
|
-
* immediate, so holding the key appended the identical block over
|
|
1181
|
-
* and over — the owner got three copies of the same four rows, each
|
|
1182
|
-
* closing with `ctrl+o opens the one before it`, a footer naming
|
|
1183
|
-
* something that does not exist.
|
|
1184
|
-
*
|
|
1185
|
-
* The bar is the BOTTOM of the transcript, not "ever shown": once
|
|
1186
|
-
* other content has arrived the expansion has scrolled up and
|
|
1187
|
-
* re-opening it is the point of the key, so the guard clears itself
|
|
1188
|
-
* the moment a cell is added.
|
|
1189
|
-
*/
|
|
1190
|
-
expandNext() {
|
|
1191
|
-
const out = this.#expandNextRaw();
|
|
1192
|
-
if (out.kind !== "appended")
|
|
1193
|
-
return out;
|
|
1194
|
-
const lines = out.lines.join("\n");
|
|
1195
|
-
if (this.#lastAppend !== null && this.#lastAppend.lines === lines && this.#lastAppend.atCells === this.#cells.length) {
|
|
1196
|
-
// NOT the same answer as "nothing is folded". The caller says
|
|
1197
|
-
// which, because a reader who pressed the key deserves to know
|
|
1198
|
-
// whether there is nothing to open or whether they are already
|
|
1199
|
-
// looking at it.
|
|
1200
|
-
return { kind: "none", why: "already-last" };
|
|
1201
|
-
}
|
|
1202
|
-
this.#lastAppend = { lines, atCells: this.#cells.length };
|
|
1203
|
-
return out;
|
|
1204
|
-
}
|
|
1205
|
-
#expandNextRaw() {
|
|
1206
|
-
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
1207
|
-
const cell = this.#cells[i];
|
|
1208
|
-
if (cell.kind === "tool" && cell.state !== "pending") {
|
|
1209
|
-
cell.expanded = !cell.expanded;
|
|
1210
|
-
this.#mark();
|
|
1211
|
-
return { kind: "toggled" };
|
|
1212
|
-
}
|
|
1213
|
-
// W20: the LIVE task block toggles in place too — the capped
|
|
1214
|
-
// form flips to the full list (the "done-collapse expands
|
|
1215
|
-
// under ctrl+o" claim). The SETTLED block is already full —
|
|
1216
|
-
// no toggle, and its rows carry no affordance, so it never
|
|
1217
|
-
// joins #collapsed (the committed /last append is moot).
|
|
1218
|
-
if (cell.kind === "checklist" && !cell.done) {
|
|
1219
|
-
cell.expanded = !cell.expanded;
|
|
1220
|
-
this.#mark();
|
|
1221
|
-
return { kind: "toggled" };
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (this.#collapsed.length === 0)
|
|
1225
|
-
return { kind: "none" };
|
|
1226
|
-
// R4 (C1) — the newest entry this cycle has not opened yet. When
|
|
1227
|
-
// every entry has been seen the cycle restarts, so the walk is
|
|
1228
|
-
// still "newest back" — it is simply immune to the ring growing
|
|
1229
|
-
// underneath it.
|
|
1230
|
-
if (this.#collapsed.every((i) => this.#opened.has(i)))
|
|
1231
|
-
this.#opened.clear();
|
|
1232
|
-
const idx = this.#collapsed.find((i) => !this.#opened.has(i)) ?? this.#collapsed[0];
|
|
1233
|
-
this.#opened.add(idx);
|
|
1234
|
-
const cell = this.#cells[idx];
|
|
1235
|
-
// R3b — a folded SEGMENT expands to the work it stands for.
|
|
1236
|
-
//
|
|
1237
|
-
// The fold line collapses a run of thinking and tool cells into
|
|
1238
|
-
// one row; without this the run would be unreachable, which is
|
|
1239
|
-
// hiding a durable record behind a summary. The rows are APPENDED
|
|
1240
|
-
// (ADR-0046 — history is never rewritten), exactly as every other
|
|
1241
|
-
// expand in this method does, and they are the cells' OWN renders,
|
|
1242
|
-
// so the expansion cannot drift from what was folded.
|
|
1243
|
-
// R13 — expandNext's FOLD branch retired with the segment fold.
|
|
1244
|
-
// A fold line collapsed a run of cells into one row, so the key had
|
|
1245
|
-
// to be able to open the run; with every call standing as its own
|
|
1246
|
-
// card there is nothing collapsed for it to open, and `#collapsed`
|
|
1247
|
-
// now holds cards alone.
|
|
1248
|
-
if (cell.kind !== "tool")
|
|
1249
|
-
return { kind: "none" };
|
|
1250
|
-
// R13 — and the ROLLUP branch retired with `rolled`.
|
|
1251
|
-
let input = {};
|
|
1252
|
-
try {
|
|
1253
|
-
input = JSON.parse(cell.inputFull);
|
|
1254
|
-
}
|
|
1255
|
-
catch {
|
|
1256
|
-
// the full JSON is always parseable (it was stringified at
|
|
1257
|
-
// toolStart) — the empty fallback never fires
|
|
1258
|
-
}
|
|
1259
|
-
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
1260
|
-
const p = palette();
|
|
1261
|
-
// 0.24.2 ③ — the expansion is a CARD, and `✦` is not its mark: that
|
|
1262
|
-
// glyph is the turn recap's, and one symbol with two meanings is
|
|
1263
|
-
// §4.1. The head row names the call, which is the tie to it that
|
|
1264
|
-
// the mark's sentence used to be.
|
|
1265
|
-
const verb = displayVerb(cell.name);
|
|
1266
|
-
const elapsed = cell.startedAt !== null && cell.doneAt !== null ? ((cell.doneAt - cell.startedAt) / 1000).toFixed(1) : "?";
|
|
1267
|
-
const n = cell.resultText === "" ? 0 : cell.resultText.split("\n").length;
|
|
1268
|
-
return {
|
|
1269
|
-
kind: "appended",
|
|
1270
|
-
lines: expandedCard(verb, toolTarget(cell.name, input), `expanded · ${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`,
|
|
1271
|
-
// TUI2-R2pre ④: the SECTION HEADERS say the act; the payloads
|
|
1272
|
-
// below them (inputFull, resultText) are RAW and byte-identical.
|
|
1273
|
-
[`--- ${verb} input ---`, cell.inputFull, `--- ${verb} output${cell.isError ? " (error)" : ""} ---`, cell.resultText], [cell.isError ? "failed" : "exit 0", `${n} line${n === 1 ? "" : "s"}`, `${elapsed}s`].filter((x) => x !== "").join(" · "), this.#opts.width()),
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
965
|
+
/* DECLARED REVERSAL (DC-50 / R14, 2026-09-05): the APPENDED expansion
|
|
966
|
+
stood here — `expandNext`, `#expandNextRaw`, `#lastAppend`, the
|
|
967
|
+
`#opened` set. ADR-0046 §3 forbade re-rendering a committed card,
|
|
968
|
+
so the only way to show a body was to print a copy further down.
|
|
969
|
+
Amendment 1 lets a settled reprint re-render every card where it
|
|
970
|
+
stands, and `toggleExpanded` is the whole feature. `#collapsed`
|
|
971
|
+
stays: the viewer (ctrl+r) reads it as its index of cut cells. */
|
|
1276
972
|
// ---- the Dock façade (the CLI's chrome API — same shape as the old dock) ----
|
|
1277
973
|
/** Docked = the chrome is live (a color TTY with a real size). */
|
|
1278
974
|
get active() {
|
|
@@ -1506,6 +1202,14 @@ export class Body {
|
|
|
1506
1202
|
onResize() {
|
|
1507
1203
|
if (!this.#isActive())
|
|
1508
1204
|
return;
|
|
1205
|
+
// R14 — the FIRST winch of a storm snapshots what the terminal is
|
|
1206
|
+
// currently holding. A drag fires many winches; the settle must
|
|
1207
|
+
// compare its end against the storm's beginning, not against the
|
|
1208
|
+
// previous winch.
|
|
1209
|
+
if (!this.#resizePending) {
|
|
1210
|
+
this.#winchFromW = this.#paintedW;
|
|
1211
|
+
this.#winchFromH = this.#paintedH;
|
|
1212
|
+
}
|
|
1509
1213
|
this.#resizePending = true;
|
|
1510
1214
|
if (this.#resizeTimer !== null)
|
|
1511
1215
|
clearTimeout(this.#resizeTimer);
|
|
@@ -1533,20 +1237,128 @@ export class Body {
|
|
|
1533
1237
|
this.#screen = [];
|
|
1534
1238
|
this.onResize();
|
|
1535
1239
|
}
|
|
1536
|
-
/**
|
|
1240
|
+
/**
|
|
1241
|
+
* R14 / route B — THE ONE REPAINT A DRAG EARNS IS A REPRINT.
|
|
1242
|
+
*
|
|
1243
|
+
* It used to be `ESC[from;1H ESC[0J` — erase from the recorded live
|
|
1244
|
+
* top down, then redraw the live area, leaving the committed rows
|
|
1245
|
+
* above to whatever the terminal's own reflow had made of them. That
|
|
1246
|
+
* is the shape R10 measured on the owner's real terminal, and it
|
|
1247
|
+
* lost: a grow dropped 16 rows of history (DC-39), a narrow
|
|
1248
|
+
* duplicated four tokens, and the scrolled-off transcript never
|
|
1249
|
+
* reflowed at all. All three are one fault — kiso doing window
|
|
1250
|
+
* arithmetic over rows the terminal had already reflowed underneath
|
|
1251
|
+
* it, with `#scrolledOff` carried across a fold change that makes
|
|
1252
|
+
* every index mean something else.
|
|
1253
|
+
*
|
|
1254
|
+
* So it stops arguing. `2J H 3J` — erase screen, home, erase
|
|
1255
|
+
* scrollback, in THAT order because on Apple Terminal a bare `2J`
|
|
1256
|
+
* scrolls the screen into history and only the following `3J` makes
|
|
1257
|
+
* the state clean — and then the whole committed transcript is
|
|
1258
|
+
* reprinted at the new geometry through the path a fresh terminal
|
|
1259
|
+
* already uses. The terminal ends holding exactly one rendering.
|
|
1260
|
+
*
|
|
1261
|
+
* The declared cost (ADR-0046 Amendment 1): everything the terminal
|
|
1262
|
+
* held before kiso started is erased at the first resize. Measured
|
|
1263
|
+
* on the reference implementation as 0/60 in every direction — this
|
|
1264
|
+
* is the behaviour being adopted, not a regression. kiso's own
|
|
1265
|
+
* record is untouched: the session log holds it, `--resume` replays
|
|
1266
|
+
* it, and G4' asserts the 0/60 so the cost cannot drift silently.
|
|
1267
|
+
*
|
|
1268
|
+
* D-B2: EVERY settled resize reprints, height-only included. A
|
|
1269
|
+
* SIGWINCH at the same geometry emits nothing at all.
|
|
1270
|
+
*/
|
|
1537
1271
|
#settleResize() {
|
|
1538
1272
|
if (!this.#resizePending || !this.#isActive())
|
|
1539
1273
|
return;
|
|
1540
1274
|
this.#resizePending = false;
|
|
1541
1275
|
const H = this.#opts.height();
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1276
|
+
const W = this.#opts.width();
|
|
1277
|
+
// D-B2's other half: a winch that did not change the geometry is
|
|
1278
|
+
// not a resize. Emitting the erase for it would throw away the
|
|
1279
|
+
// terminal's scrollback for nothing (the V6-1 idempotence case).
|
|
1280
|
+
if (this.#winchFromW === W && this.#winchFromH === H)
|
|
1281
|
+
return;
|
|
1282
|
+
// §9.2 — the viewer is closed first. Nothing commits while it is
|
|
1283
|
+
// up and a reprint is a commit storm; the user reopens it. One
|
|
1284
|
+
// keypress, stated rather than hidden.
|
|
1285
|
+
if (this.#viewer !== null) {
|
|
1286
|
+
this.#viewer = null;
|
|
1287
|
+
this.#viewerWasUp = true;
|
|
1288
|
+
}
|
|
1289
|
+
this.#reprint();
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* R14 — ERASE THE TERMINAL AND PRINT THE SESSION AGAIN.
|
|
1293
|
+
*
|
|
1294
|
+
* Two callers: a settled resize, and DC-50's ctrl+o. They are the
|
|
1295
|
+
* same act — the rendering the terminal holds is wrong (wrong
|
|
1296
|
+
* geometry, or wrong expansion state) and the model is the only
|
|
1297
|
+
* authority on what it should be — so they share the path rather
|
|
1298
|
+
* than growing two.
|
|
1299
|
+
*/
|
|
1300
|
+
#reprint() {
|
|
1301
|
+
const H = this.#opts.height();
|
|
1302
|
+
this.#write("\x1b[2J\x1b[H\x1b[3J");
|
|
1303
|
+
// The terminal now holds nothing, so every record of what it held
|
|
1304
|
+
// is void. `#scrolledOff` is the frontier of what reached its
|
|
1305
|
+
// scrollback: after the erase, that is zero — which is also what
|
|
1306
|
+
// releases the committed cells to be re-rendered at the new width
|
|
1307
|
+
// without contradicting anything, the thing DC-34's frontier rule
|
|
1308
|
+
// existed to prevent.
|
|
1309
|
+
this.#scrolledOff = 0;
|
|
1310
|
+
for (let i = 0; i < this.#committed; i += 1)
|
|
1311
|
+
this.#lineCache[i] = null;
|
|
1312
|
+
this.#screen = new Array(Math.max(1, H)).fill(NOT_PAINTED);
|
|
1313
|
+
this.#cursorRow = 1;
|
|
1545
1314
|
this.#fullRedraw = true;
|
|
1546
1315
|
this.#resizeFrame = true;
|
|
1547
1316
|
this.#dirty = true;
|
|
1548
1317
|
this.render();
|
|
1549
1318
|
}
|
|
1319
|
+
/**
|
|
1320
|
+
* DC-50 — ctrl+o flips ONE switch, and every settled card obeys it.
|
|
1321
|
+
*
|
|
1322
|
+
* The walk this replaces existed because ADR-0046 §3 forbade
|
|
1323
|
+
* re-rendering a committed card, so the only way to show one's body
|
|
1324
|
+
* was to APPEND a copy further down — with a ring to decide which
|
|
1325
|
+
* card was next, `#opened` to keep the ring from repeating, and
|
|
1326
|
+
* `#lastAppend` to stop a held key printing the same rows three times
|
|
1327
|
+
* (DC-35). Amendment 1 removes the premise: the scrollback is ours to
|
|
1328
|
+
* erase, so a card can simply be re-rendered where it stands.
|
|
1329
|
+
*
|
|
1330
|
+
* A RUNNING card is exempt. Its height is E2/DC-43's — it grows from
|
|
1331
|
+
* its own content — and a global "show everything" has no business
|
|
1332
|
+
* reaching into a card whose content is still arriving.
|
|
1333
|
+
*/
|
|
1334
|
+
toggleExpanded() {
|
|
1335
|
+
this.#expandedAll = !this.#expandedAll;
|
|
1336
|
+
for (const cell of this.#cells) {
|
|
1337
|
+
// SETTLED CONTENT, not "done". A card parked for approval is
|
|
1338
|
+
// `state: "approval"`, `done: false` — and its content is not
|
|
1339
|
+
// still arriving: the diff is complete and the card is waiting
|
|
1340
|
+
// for a human to read it and decide. The mechanism this
|
|
1341
|
+
// replaces toggled it (any state but "pending"), and the
|
|
1342
|
+
// dispatch comment said why: "the approval pause is exactly
|
|
1343
|
+
// when the user reads a cut diff, and the key must answer then,
|
|
1344
|
+
// never after the run." A `done`-only switch answers with
|
|
1345
|
+
// nothing at the one moment the answer matters most.
|
|
1346
|
+
//
|
|
1347
|
+
// The exemption is for the card still GROWING — E2 and DC-43
|
|
1348
|
+
// own its height, and a global "show everything" has no
|
|
1349
|
+
// business reaching into content that is still arriving. That
|
|
1350
|
+
// is the ruling's reason; `done` alone was its wording.
|
|
1351
|
+
if (cell.kind !== "tool")
|
|
1352
|
+
continue;
|
|
1353
|
+
if (cell.done || cell.state === "approval")
|
|
1354
|
+
cell.expanded = this.#expandedAll;
|
|
1355
|
+
}
|
|
1356
|
+
this.#reprint();
|
|
1357
|
+
}
|
|
1358
|
+
/** DC-50 — the switch itself, for the CLI's affordance text. */
|
|
1359
|
+
expandedAll() {
|
|
1360
|
+
return this.#expandedAll;
|
|
1361
|
+
}
|
|
1550
1362
|
/** W18: the status row's right-aligned hint is part of the status
|
|
1551
1363
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
1552
1364
|
* must survive repaints). */
|
|
@@ -1584,13 +1396,21 @@ export class Body {
|
|
|
1584
1396
|
// approval and the pick, ask-panel.ts for the ask), so nothing is
|
|
1585
1397
|
// lost anywhere.
|
|
1586
1398
|
if (panel !== null)
|
|
1587
|
-
return { status: panelStatusOf(panel), hint: undefined };
|
|
1399
|
+
return { status: panelStatusOf(panel), hint: undefined, expand: null };
|
|
1588
1400
|
// W22: while turns wait in the queue, the right hint shows the
|
|
1589
1401
|
// count — the chips below carry the lines themselves.
|
|
1590
1402
|
const queued = this.#queueState?.().length ?? 0;
|
|
1591
1403
|
if (queued > 0)
|
|
1592
|
-
return { status: this.#status, hint: `+${queued} queued
|
|
1593
|
-
|
|
1404
|
+
return { status: this.#status, hint: `+${queued} queued`, expand: null };
|
|
1405
|
+
// D-S2-1 (owner-ruled 2026-09-06): the idle hint names the ctrl+o
|
|
1406
|
+
// SWITCH — `expand all` or `collapse all` by its state — and only
|
|
1407
|
+
// while a committed card has something behind the key (#collapsed
|
|
1408
|
+
// is the index of exactly those). A hint for a key that would
|
|
1409
|
+
// change nothing is a hint that lies. This replaced the per-card
|
|
1410
|
+
// bright token (TUI2-R2 ⑤), which had no single target left to
|
|
1411
|
+
// mark once DC-50 made the key global.
|
|
1412
|
+
const expand = this.#collapsed.length > 0 ? (this.#expandedAll ? "collapse all" : "expand all") : null;
|
|
1413
|
+
return { status: this.#status, hint: this.#statusHint ?? undefined, expand };
|
|
1594
1414
|
}
|
|
1595
1415
|
/** Bind the editor's panel state — the PanelSelect slot occupant
|
|
1596
1416
|
* (W21: the panel replaces the live region + the input lead while
|
|
@@ -1721,49 +1541,17 @@ export class Body {
|
|
|
1721
1541
|
}
|
|
1722
1542
|
// ---- the one writer ----
|
|
1723
1543
|
/**
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
* the
|
|
1733
|
-
*
|
|
1734
|
-
* its count rides the line.
|
|
1735
|
-
*
|
|
1736
|
-
* What it fixes: a 28-call turn used to spend 28 rows of a 30-row
|
|
1737
|
-
* live region, so overflow was the NORM on real turns rather than
|
|
1738
|
-
* the edge — and a turn that overflows may not fold (R3f: a line
|
|
1739
|
-
* cannot claim rows already in the scrollback), which is why the
|
|
1740
|
-
* fold missed exactly the turns it exists for. The block's height
|
|
1741
|
-
* no longer depends on the call count at all.
|
|
1742
|
-
*
|
|
1743
|
-
* What it does NOT change: nothing about what commits or when. The
|
|
1744
|
-
* hold is untouched, the force-commit cap is untouched, and the
|
|
1745
|
-
* settle still produces the same fold it did before. That is the
|
|
1746
|
-
* charter's line between this phase and the next.
|
|
1544
|
+
* THE LIVE PROJECTION — the uncommitted cells as rows, one definition
|
|
1545
|
+
* for the natural path and the force-commit loop alike (two copies of
|
|
1546
|
+
* "what the live region looks like" would be two answers to one
|
|
1547
|
+
* question). The live region is bounded by the SCREEN, and nothing
|
|
1548
|
+
* else: DC-46's `#liveRoom` capped it at `H − chrome − #committedLines`
|
|
1549
|
+
* to keep the window's top monotone, but `#committedLines` is
|
|
1550
|
+
* cumulative, so one screenful into any session every running call
|
|
1551
|
+
* was a head row with its output gone. The window's top is held by
|
|
1552
|
+
* the `skip` clamp in render() instead; a live region that grows
|
|
1553
|
+
* scrolls committed rows away, which is an append and not an un-scroll.
|
|
1747
1554
|
*/
|
|
1748
|
-
/* DC-46 — `#liveRoom` RETIRED, and the reasoning with it.
|
|
1749
|
-
It capped the live region at `H − chrome − #committedLines`, to
|
|
1750
|
-
keep `skip` a function of `#committedLines` alone and therefore
|
|
1751
|
-
monotone. The argument was right about `skip` and wrong about the
|
|
1752
|
-
quantity: `#committedLines` is CUMULATIVE — re-derived over the
|
|
1753
|
-
whole line cache every frame, counting rows that left for the
|
|
1754
|
-
terminal's scrollback long ago — so one screenful into any session
|
|
1755
|
-
it exceeds H, the room clamps to its floor of one row, and every
|
|
1756
|
-
running call after that is a head row with its output gone.
|
|
1757
|
-
Measured at 24 and 40 rows with eight blank rows still on screen.
|
|
1758
|
-
|
|
1759
|
-
The live region is bounded by the SCREEN (the content cap the
|
|
1760
|
-
force-commit loop already uses). What holds the window's top is
|
|
1761
|
-
the `skip` clamp below: rows in [0, #scrolledOff) have reached the
|
|
1762
|
-
terminal's scrollback and are immutable, so the paint may not go
|
|
1763
|
-
back above them. A live region that GROWS scrolls committed rows
|
|
1764
|
-
away through #emitScroll, which is an append and not an un-scroll.
|
|
1765
|
-
The residue is a transient hole above the composer at a settle,
|
|
1766
|
-
bounded by the shrink itself and filled by the next commit. */
|
|
1767
1555
|
#liveProjection(W, ctx, cap) {
|
|
1768
1556
|
const rows = this.#project(W, ctx, CAP_PREVIEW);
|
|
1769
1557
|
if (cap === undefined || rows.length <= cap)
|
|
@@ -1804,39 +1592,19 @@ export class Body {
|
|
|
1804
1592
|
return [...heads.slice(0, keep), cutLine(`${p.dim} +${hidden} more${p.reset}`, W)];
|
|
1805
1593
|
}
|
|
1806
1594
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
1813
|
-
* nothing, so the live region's height was independent of the call
|
|
1814
|
-
* count. That was the answer to a 28-call turn spending 28 rows of a
|
|
1815
|
-
* 30-row region.
|
|
1816
|
-
*
|
|
1817
|
-
* The card answers it differently, and the ruling prefers this
|
|
1818
|
-
* answer: a running call is its own card at a FIXED height (E2), so
|
|
1819
|
-
* the height is a function of how many calls are IN FLIGHT rather
|
|
1820
|
-
* than of how many have happened — and the window shrinks to the room
|
|
1821
|
-
* before anything is force-committed (above). The live form and the
|
|
1822
|
-
* committed form are now the same form, which is what makes a settle
|
|
1823
|
-
* a change of content and never of position.
|
|
1824
|
-
*
|
|
1825
|
-
* What this keeps from R4: the height never moves ON ITS OWN. What it
|
|
1826
|
-
* gives up: the one-line summary of a stretch, which the owner ruled
|
|
1827
|
-
* costs more than it buys.
|
|
1595
|
+
* ONE PASS, AND EVERY CELL RENDERS ITSELF. The live form and the
|
|
1596
|
+
* committed form are the same form, which is what makes a settle a
|
|
1597
|
+
* change of content and never of position (DECLARED REVERSAL, R13:
|
|
1598
|
+
* R3i's stretch line and R4's standing slot drew one line plus a
|
|
1599
|
+
* fixed block for the whole stretch and had every other cell draw
|
|
1600
|
+
* nothing; the owner ruled the summary costs more than it buys).
|
|
1828
1601
|
*/
|
|
1829
1602
|
#project(W, ctx, budget) {
|
|
1830
1603
|
const out = [];
|
|
1831
|
-
const focus = this.#focusIndex();
|
|
1832
1604
|
const live = { ...ctx, liveWindow: budget };
|
|
1833
1605
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1834
1606
|
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1835
1607
|
const rows = cellComponent(this.#cells[i]).render(W, live);
|
|
1836
|
-
// the head row carries the affordance; the tint lands on it and
|
|
1837
|
-
// nowhere else, which is what makes "exactly one" structural
|
|
1838
|
-
if (i === focus && rows.length > 0)
|
|
1839
|
-
rows[0] = focusToken(rows[0], W);
|
|
1840
1608
|
out.push(...this.#space(i, prev, rows));
|
|
1841
1609
|
prev = rows;
|
|
1842
1610
|
}
|
|
@@ -1883,56 +1651,17 @@ export class Body {
|
|
|
1883
1651
|
* blanks are join artifacts — the count includes them (they are real
|
|
1884
1652
|
* screen rows), threaded against the previous sibling's OWN rows. */
|
|
1885
1653
|
liveCount() {
|
|
1886
|
-
const panel = this.#panelState?.() ?? null;
|
|
1887
|
-
const sheet = this.#sheetState?.() === true;
|
|
1888
|
-
const queueRows = this.#queueRows(this.#opts.width(), this.#opts.height());
|
|
1889
|
-
// KC1 §6: the composer's extra rows are chrome too — the scalar
|
|
1890
|
-
// counts them exactly like the menu/queue bands (N = 1 ⇒ +0)
|
|
1891
|
-
const inputExtra = this.#inputRows(this.#opts.width(), this.#opts.height(), this.#menuRows(this.#opts.width()).length, queueRows.length).rows.length - 1;
|
|
1892
|
-
// TUI2-R1 (D): the sheet occupies the live region, exactly like the
|
|
1893
|
-
// panel — the scalar must say so, or the cap arithmetic disagrees
|
|
1894
|
-
// with the screen.
|
|
1895
|
-
// R5 — the viewer occupies the live region exactly like the sheet,
|
|
1896
|
-
// so the scalar must say so, or the cap arithmetic disagrees with
|
|
1897
|
-
// the screen (the same rule DC-27 was about).
|
|
1898
|
-
if (this.#viewer !== null) {
|
|
1899
|
-
const capV = Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length);
|
|
1900
|
-
return this.#viewerBand(this.#opts.width()).slice(0, capV).length + CHROME_ROWS + inputExtra + queueRows.length;
|
|
1901
|
-
}
|
|
1902
|
-
if (sheet) {
|
|
1903
|
-
return (keysSheetRows(this.#opts.width()).slice(0, Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
1904
|
-
CHROME_ROWS +
|
|
1905
|
-
inputExtra +
|
|
1906
|
-
queueRows.length);
|
|
1907
|
-
}
|
|
1908
|
-
if (panel !== null) {
|
|
1909
|
-
// W21: the panel's own rows (the cap is exact — the scalar
|
|
1910
|
-
// reflects the screen). W22: the queue chips occupy their
|
|
1911
|
-
// own band — the panel's cap shrinks by their rows.
|
|
1912
|
-
return (panelRowsOf(panel, this.#opts.width(), Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
1913
|
-
CHROME_ROWS +
|
|
1914
|
-
inputExtra +
|
|
1915
|
-
queueRows.length);
|
|
1916
|
-
}
|
|
1917
|
-
// DC-27 — the scalar measures the PROJECTION, not a second render
|
|
1918
|
-
// of its own. This loop used to walk every live cell and render it
|
|
1919
|
-
// in full: no open-segment collapse, no flight rule, no act-slot
|
|
1920
|
-
// budget. After R3i that described a screen the compositor had
|
|
1921
|
-
// stopped drawing — for an open stretch with five finished calls
|
|
1922
|
-
// it counted five four-row blocks that were not there. Nothing
|
|
1923
|
-
// broke, because the force-commit loop measures liveLines.length
|
|
1924
|
-
// and the over-count is conservative; but the cap and geometry
|
|
1925
|
-
// gates were asserting a property of a function nothing paints
|
|
1926
|
-
// from, so a real regression in the region's height could not
|
|
1927
|
-
// have moved them. The rule this file already states for the
|
|
1928
|
-
// sheet ("the scalar must say so, or the cap arithmetic disagrees
|
|
1929
|
-
// with the screen") is the same rule here.
|
|
1930
|
-
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
1931
1654
|
const W = this.#opts.width();
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1655
|
+
const H = this.#opts.height();
|
|
1656
|
+
const chrome = this.#chrome(W, H);
|
|
1657
|
+
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
1658
|
+
// DC-27 — the scalar measures what the SCREEN gets: the same
|
|
1659
|
+
// selector render() paints from, under the same chrome. A scalar
|
|
1660
|
+
// with rules of its own asserted a property of a function nothing
|
|
1661
|
+
// painted from, so the cap and geometry gates could not move on a
|
|
1662
|
+
// real regression ("the scalar must say so, or the cap arithmetic
|
|
1663
|
+
// disagrees with the screen" — TUI2-R1 D, R5, DC-27, one rule).
|
|
1664
|
+
return this.#liveRows(W, ctx, chrome.cap).lines.length + chrome.rows;
|
|
1936
1665
|
}
|
|
1937
1666
|
/** The lines committed THIS frame — the writes land in the frame's
|
|
1938
1667
|
* committed section (the rows just above the live region). */
|
|
@@ -1949,243 +1678,161 @@ export class Body {
|
|
|
1949
1678
|
if (H < 4)
|
|
1950
1679
|
return;
|
|
1951
1680
|
this.#lastH = H;
|
|
1681
|
+
// R14 — the geometry THIS frame painted. `onResize` snapshots it
|
|
1682
|
+
// at the moment the winch arrives; see `#winchFromW`.
|
|
1683
|
+
this.#paintedW = W;
|
|
1684
|
+
this.#paintedH = H;
|
|
1952
1685
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
// the
|
|
1956
|
-
|
|
1957
|
-
//
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
//
|
|
1962
|
-
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
//
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
//
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
// The second is not a compromise of the first, it is the
|
|
2003
|
-
// answer to a question the first cannot reach. A cell can
|
|
2004
|
-
// STRADDLE the frontier — its head in the scrollback, its
|
|
2005
|
-
// tail still on screen — and the tail must be painted at the
|
|
2006
|
-
// current width. Holding its commit fold there emitted a
|
|
2007
|
-
// 100-column row into an 80-column screen and invariant ①
|
|
2008
|
-
// threw (60 → 100 → 80, measured). Fitting wins: a crash is
|
|
2009
|
-
// worse than a seam, and the seam a narrowing leaves is
|
|
2010
|
-
// rider 2's, stated rather than hidden.
|
|
2011
|
-
let row = 0;
|
|
2012
|
-
const refold = new Array(this.#committed).fill(false);
|
|
2013
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2014
|
-
const lines = this.#lineCache[i];
|
|
2015
|
-
if (lines === null || lines === undefined) {
|
|
2016
|
-
refold[i] = true;
|
|
2017
|
-
continue;
|
|
2018
|
-
}
|
|
2019
|
-
const above = row >= this.#scrolledOff;
|
|
2020
|
-
const fits = lines.every((l) => visibleWidth(l) <= W);
|
|
2021
|
-
refold[i] = above || !fits;
|
|
2022
|
-
const prev = i > 0 ? this.#lineCache[i - 1] : null;
|
|
2023
|
-
row += this.#space(i, prev ?? null, lines).length;
|
|
2024
|
-
}
|
|
2025
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2026
|
-
if (refold[i])
|
|
2027
|
-
this.#lineCache[i] = cellComponent(this.#cells[i]).render(W, ctx);
|
|
2028
|
-
}
|
|
2029
|
-
// #committedLines is re-derived over the WHOLE cache, because
|
|
2030
|
-
// the frozen prefix still occupies its own rows.
|
|
2031
|
-
this.#committedLines = 0;
|
|
2032
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2033
|
-
const lines = this.#lineCache[i] ?? cellComponent(this.#cells[i]).render(W, ctx);
|
|
2034
|
-
this.#lineCache[i] = lines;
|
|
2035
|
-
this.#committedLines += this.#space(i, i > 0 ? (this.#lineCache[i - 1] ?? []) : null, lines).length;
|
|
2036
|
-
}
|
|
2037
|
-
this.#refolded = refold.some(Boolean);
|
|
2038
|
-
}
|
|
2039
|
-
// 1. the natural commits — the leading DONE cells freeze: their
|
|
2040
|
-
// lines leave the live region, the scrolls + the committed
|
|
2041
|
-
// writes below place them (the #17 "freeze as a real line",
|
|
2042
|
-
// short sessions included — the frame coalescing keeps a
|
|
2043
|
-
// cell's first frame its freeze frame, so the frozen bytes
|
|
2044
|
-
// emit exactly once).
|
|
1686
|
+
if (this.#fullRedraw)
|
|
1687
|
+
this.#rederiveCommitted(W, ctx);
|
|
1688
|
+
// 1. the natural commits — the leading DONE cells freeze.
|
|
1689
|
+
this.#commitDone(W, ctx);
|
|
1690
|
+
// 2. the live lines — the unfinished cells (the tail) + the chrome.
|
|
1691
|
+
const chrome = this.#chrome(W, H);
|
|
1692
|
+
this.#noteOverlay();
|
|
1693
|
+
const live = this.#liveRows(W, ctx, chrome.cap);
|
|
1694
|
+
// 3. the FORCE commits — the live region's hard cap.
|
|
1695
|
+
const liveLines = this.#capLive(live.lines, W, ctx, chrome.cap);
|
|
1696
|
+
// 4. the geometry — the live region's first row:
|
|
1697
|
+
// liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
|
|
1698
|
+
// shows the bottom H rows; the live region anchors to the bottom.
|
|
1699
|
+
const liveRowsTotal = liveLines.length + chrome.rows;
|
|
1700
|
+
const liveTop = Math.min(this.#committedLines, H - liveRowsTotal) + 1;
|
|
1701
|
+
// TUI2-R3v2 ②: the option rows' ABSOLUTE screen rows, recorded per
|
|
1702
|
+
// frame. A click is answered against the frame the human was looking
|
|
1703
|
+
// at when they clicked, which is this one.
|
|
1704
|
+
this.#panelRowSpan =
|
|
1705
|
+
live.panelSpan === null ? null : { top: liveTop + live.panelSpan.offset, count: live.panelSpan.count, first: live.panelSpan.first };
|
|
1706
|
+
// 5. the frame bytes.
|
|
1707
|
+
this.#paint(W, H, liveTop, liveLines, liveRowsTotal, chrome);
|
|
1708
|
+
}
|
|
1709
|
+
/** Phase 0 — a full redraw re-derives the committed rows from the
|
|
1710
|
+
* cache. A cell whose cache was invalidated (a settled resize, R14)
|
|
1711
|
+
* is rendered again at the current width; every other cell keeps
|
|
1712
|
+
* the rows it was committed with, because those rows are already
|
|
1713
|
+
* the terminal's (ADR-0046) and this frame paints the same bytes.
|
|
1714
|
+
*
|
|
1715
|
+
* DECLARED REVERSAL (R14): DC-34's frontier-scoped REFOLD stood
|
|
1716
|
+
* here — a widen never refolded a committed cell, a narrowing
|
|
1717
|
+
* refolded the cells that no longer fit. Route B reprints the
|
|
1718
|
+
* whole session from the model on a settled resize, so no
|
|
1719
|
+
* committed row is refolded in place any more, and the width
|
|
1720
|
+
* guard that decided which ones were went with it. */
|
|
1721
|
+
#rederiveCommitted(W, ctx) {
|
|
1722
|
+
this.#committedLines = 0;
|
|
1723
|
+
for (let i = 0; i < this.#committed; i += 1) {
|
|
1724
|
+
const lines = this.#lineCache[i] ?? cellComponent(this.#cells[i]).render(W, ctx);
|
|
1725
|
+
this.#lineCache[i] = lines;
|
|
1726
|
+
this.#committedLines += this.#space(i, i > 0 ? (this.#lineCache[i - 1] ?? []) : null, lines).length;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Phase 1 — the natural commits: the leading DONE cells freeze —
|
|
1730
|
+
* their lines leave the live region, the scrolls + the committed
|
|
1731
|
+
* writes below place them (the #17 "freeze as a real line", short
|
|
1732
|
+
* sessions included — the frame coalescing keeps a cell's first
|
|
1733
|
+
* frame its freeze frame, so the frozen bytes emit exactly once). */
|
|
1734
|
+
#commitDone(W, ctx) {
|
|
2045
1735
|
this.#committedAtFrameStart = this.#committed;
|
|
2046
1736
|
this.#committedLinesThisFrame = [];
|
|
2047
|
-
|
|
2048
|
-
// cell of the OPEN quiet turn (no text yet) does not commit: its
|
|
2049
|
-
// committed form is decided at the release (the turn's text →
|
|
2050
|
-
// individual commits with the W13 rollups; the turn's end → the
|
|
2051
|
-
// fold). The FORCE-commit path below bypasses the hold — the
|
|
2052
|
-
// screen never sticks, the rollup degrades to individuals.
|
|
2053
|
-
while (this.#committed < this.#cells.length && this.#cells[this.#committed].done && !this.#held(this.#committed)) {
|
|
1737
|
+
while (this.#committed < this.#cells.length && this.#cells[this.#committed].done) {
|
|
2054
1738
|
this.#commitCell(this.#committed, W, ctx);
|
|
2055
1739
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
1740
|
+
}
|
|
1741
|
+
/** The chrome this frame wears: the menu band, the queue band (W22:
|
|
1742
|
+
* the menu-rows family's other occupant — the band above the box
|
|
1743
|
+
* top), the composer (KC1 §6: N rows; N = 1 ⇒ today's chrome
|
|
1744
|
+
* exactly — chromeRows = 3 + N + menu + queue, and the content cap
|
|
1745
|
+
* loses the composer's EXTRA rows the same way it loses the bands).
|
|
1746
|
+
* W11: the formula's blank above the first live cell hangs off the
|
|
1747
|
+
* last COMMITTED sibling (the join spans the boundary). */
|
|
1748
|
+
#chrome(W, H) {
|
|
2059
1749
|
const menuRows = this.#menuRows(W);
|
|
2060
|
-
// W22: the queue chips are the menu-rows family's other occupant
|
|
2061
|
-
// (the band above the box top) — the chrome rows and the live
|
|
2062
|
-
// caps account for both.
|
|
2063
1750
|
const queueRows = this.#queueRows(W, H);
|
|
2064
|
-
// KC1 §6: the input is N rows now (N = 1 ⇒ today's chrome exactly)
|
|
2065
|
-
// — chromeRows = 3 + N + menu + queue, and the content cap loses
|
|
2066
|
-
// the composer's EXTRA rows the same way it loses the bands.
|
|
2067
1751
|
const editor = this.#inputRows(W, H, menuRows.length, queueRows.length);
|
|
2068
1752
|
const inputExtra = editor.rows.length - 1;
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
1753
|
+
return {
|
|
1754
|
+
menuRows,
|
|
1755
|
+
queueRows,
|
|
1756
|
+
editor,
|
|
1757
|
+
inputExtra,
|
|
1758
|
+
rows: CHROME_ROWS + inputExtra + menuRows.length + queueRows.length,
|
|
1759
|
+
cap: H - 4 - inputExtra - queueRows.length,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
/** The overlay bookkeeping. TUI2-R1.5 ⑦(a) (VD-8): the sheet is an
|
|
1763
|
+
* OVERLAY, and the frame it opens on — and the one it closes on —
|
|
1764
|
+
* take the full-redraw path. The sheet REPLACES the live region, so
|
|
1765
|
+
* on an idle composer (where the live region is empty) opening it
|
|
1766
|
+
* GROWS the model by its own height; the frame's skip grows with it
|
|
1767
|
+
* and the difference is paid in real LFs — rows scrolled
|
|
1768
|
+
* permanently into the terminal's scrollback, which closing cannot
|
|
1769
|
+
* undo, because the scrollback is not ours to rewrite. Measured:
|
|
1770
|
+
* three rows per open on a full screen. The overlay displaces
|
|
1771
|
+
* content on screen instead. R5 — the viewer is an overlay of
|
|
1772
|
+
* exactly the same kind, so it joins the same flag. That one word
|
|
1773
|
+
* is what buys it the whole zero-litter discipline: the window
|
|
1774
|
+
* freezes, #emitScroll is skipped, and the close repaints from
|
|
1775
|
+
* #lastSkip. */
|
|
1776
|
+
#noteOverlay() {
|
|
2085
1777
|
const sheetUp = this.#sheetState?.() === true;
|
|
2086
|
-
// R5 — the viewer is an overlay of exactly the same kind, so it
|
|
2087
|
-
// joins the same flag. That one word is what buys it the whole
|
|
2088
|
-
// zero-litter discipline below: the window freezes, #emitScroll
|
|
2089
|
-
// is skipped, and the close repaints from #lastSkip.
|
|
2090
1778
|
const viewerUp = this.#viewer !== null;
|
|
2091
1779
|
this.#overlayFrame = sheetUp || this.#sheetWasUp || viewerUp || this.#viewerWasUp;
|
|
2092
1780
|
this.#sheetWasUp = sheetUp;
|
|
2093
1781
|
this.#viewerWasUp = viewerUp;
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
const frame = panelFrameOf(panel, W,
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
//
|
|
2135
|
-
//
|
|
2136
|
-
// This loop committed `#committed` unconditionally. R4's standing
|
|
2137
|
-
// slot held the live region at a constant height, so the
|
|
2138
|
-
// projection never grew past the cap on its own and the loop never
|
|
2139
|
-
// reached a running cell; R13 retired the slot (DC-46) and it
|
|
2140
|
-
// promptly did. Three parallel searches, the last two settling
|
|
2141
|
-
// first: their cards pushed the region over the cap, the loop
|
|
2142
|
-
// committed the FIRST call — still in flight — froze its
|
|
2143
|
-
// three-row running card and its breathing mark into the
|
|
2144
|
-
// scrollback, and stepped `#committed` past it. When the result
|
|
2145
|
-
// arrived there was no live cell left to draw it into, so the work
|
|
2146
|
-
// never reached the screen at all.
|
|
2147
|
-
//
|
|
2148
|
-
// A cell that is not done has no committed form yet; committing
|
|
2149
|
-
// one is writing history that has not happened. When the head is
|
|
2150
|
-
// running, the region gives way instead — the cards behind it
|
|
2151
|
-
// degrade first, then the running window shrinks (DC-43's own
|
|
2152
|
-
// ladder, in #liveProjection), and the remainder is counted.
|
|
2153
|
-
// The guard is on a RUNNING TOOL CALL specifically, not on any
|
|
2154
|
-
// unfinished cell. A streaming text or raw cell HAS a committed
|
|
2155
|
-
// form — the rows it has already written are final, append-only —
|
|
2156
|
-
// and spilling those is exactly what this loop is for. A running
|
|
2157
|
-
// call does not: its card changes shape at the settle, so a
|
|
2158
|
-
// committed one is a row that will never be corrected.
|
|
2159
|
-
while (liveLines.length > H - 4 - inputExtra - queueRows.length && // V6-3: the content cap H−4 (KC1: −N's extra rows)
|
|
1782
|
+
}
|
|
1783
|
+
/** Phase 2 — ONE overlay selector: what occupies the live region
|
|
1784
|
+
* this frame. The transcript viewer (R5), the keys sheet (TUI2-R1
|
|
1785
|
+
* D) and the approval panel (W21) each REPLACE the live region —
|
|
1786
|
+
* it is what the human is reading right now — capped at the
|
|
1787
|
+
* content cap; the sheet and the viewer open only from an idle
|
|
1788
|
+
* composer, so neither can coexist with a panel. The panel also
|
|
1789
|
+
* reports where its clickable rows are (TUI2-R3v2 ②: the rows and
|
|
1790
|
+
* the span come from one call, so the hit-test reads the arithmetic
|
|
1791
|
+
* that placed the rows). Otherwise the projection of the live
|
|
1792
|
+
* cells. render() paints this and
|
|
1793
|
+
* liveCount() measures it — the same rows, by construction. */
|
|
1794
|
+
#liveRows(W, ctx, cap) {
|
|
1795
|
+
const capped = Math.max(1, cap);
|
|
1796
|
+
if (this.#viewer !== null)
|
|
1797
|
+
return { lines: this.#viewerBand(W).slice(0, capped), panelSpan: null };
|
|
1798
|
+
if (this.#sheetState?.() === true)
|
|
1799
|
+
return { lines: keysSheetRows(W).slice(0, capped), panelSpan: null };
|
|
1800
|
+
const panel = this.#panelState?.() ?? null;
|
|
1801
|
+
if (panel !== null) {
|
|
1802
|
+
// W21: the panel's cap is exact, so the force-commit loop never
|
|
1803
|
+
// fires on it. W22: the queue band sits below the panel — the
|
|
1804
|
+
// cap shrinks by it.
|
|
1805
|
+
const frame = panelFrameOf(panel, W, capped);
|
|
1806
|
+
return { lines: frame.rows, panelSpan: frame.options };
|
|
1807
|
+
}
|
|
1808
|
+
return { lines: this.#liveProjection(W, ctx, cap), panelSpan: null };
|
|
1809
|
+
}
|
|
1810
|
+
/** Phase 3 — the FORCE commits: overflow past the content cap
|
|
1811
|
+
* commits the oldest live cell (W22: the queue band shrinks the
|
|
1812
|
+
* cap by its rows) — but NEVER A TOOL CALL STILL RUNNING (DC-53).
|
|
1813
|
+
* A running card has no committed form yet: its shape changes at
|
|
1814
|
+
* the settle, so a committed one would be a row that is never
|
|
1815
|
+
* corrected — measured as a burst's first call frozen into the
|
|
1816
|
+
* scrollback with its breathing mark, its result never drawn. A
|
|
1817
|
+
* streaming text or raw cell DOES have a committed form (its rows
|
|
1818
|
+
* are final, append-only) and spills normally. When the head is
|
|
1819
|
+
* running the region gives way instead: the cards behind it
|
|
1820
|
+
* degrade, then the window shrinks (#liveProjection). */
|
|
1821
|
+
#capLive(lines, W, ctx, cap) {
|
|
1822
|
+
while (lines.length > cap && // V6-3: the content cap H−4 (KC1: −N's extra rows)
|
|
2160
1823
|
this.#committed < this.#cells.length &&
|
|
2161
1824
|
!(this.#cells[this.#committed].kind === "tool" && !this.#cells[this.#committed].done)) {
|
|
2162
|
-
// R3f: the cell about to be force-committed marks its segment
|
|
2163
|
-
// SPILLED. The rule was written at R3b — "a segment too big for
|
|
2164
|
-
// the screen already has rows in the scrollback that cannot be
|
|
2165
|
-
// taken back, so it renders normally and does not collapse" —
|
|
2166
|
-
// and then never wired: `spilled` had a declaration, an
|
|
2167
|
-
// initializer and a read, and nothing ever set it. The read was
|
|
2168
|
-
// therefore vacuously true, so a 43-call turn force-committed
|
|
2169
|
-
// thirty expanded rows and STILL printed `✦ thought 103s · 43
|
|
2170
|
-
// reads` underneath them, claiming as folded the work standing
|
|
2171
|
-
// visible above it.
|
|
2172
|
-
this.#markSpilled(this.#committed);
|
|
2173
1825
|
this.#commitCell(this.#committed, W, ctx);
|
|
2174
1826
|
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
2175
1827
|
// pointed at may have just left the live region.
|
|
2176
|
-
|
|
1828
|
+
lines = this.#liveProjection(W, ctx, cap);
|
|
2177
1829
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
// frame. A click is answered against the frame the human was looking
|
|
2185
|
-
// at when they clicked, which is this one.
|
|
2186
|
-
this.#panelRowSpan =
|
|
2187
|
-
panelSpan === null ? null : { top: liveTop + panelSpan.offset, count: panelSpan.count, first: panelSpan.first };
|
|
2188
|
-
// 5. the frame bytes.
|
|
1830
|
+
return lines;
|
|
1831
|
+
}
|
|
1832
|
+
/** Phase 5 — the frame bytes: the one-time reset, autowrap off, the
|
|
1833
|
+
* synchronised-update bracket, the ONE renderer, and the
|
|
1834
|
+
* bookkeeping the next frame's relative moves start from. */
|
|
1835
|
+
#paint(W, H, liveTop, liveLines, liveRowsTotal, chrome) {
|
|
2189
1836
|
const out = [];
|
|
2190
1837
|
// REL-0152-D14 — AUTOWRAP OFF for the frame's duration.
|
|
2191
1838
|
//
|
|
@@ -2266,28 +1913,12 @@ export class Body {
|
|
|
2266
1913
|
// never paired with a ?25h. Sync (2026) still brackets the frame
|
|
2267
1914
|
// where the terminal understands it.
|
|
2268
1915
|
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h\x1b[?25l");
|
|
2269
|
-
//
|
|
2270
|
-
//
|
|
2271
|
-
//
|
|
2272
|
-
//
|
|
2273
|
-
//
|
|
2274
|
-
|
|
2275
|
-
// stale pass erases the old live rows, and nothing re-paints the
|
|
2276
|
-
// band between the committed window and the new liveTop (finding
|
|
2277
|
-
// #A8 — the W11-boundary pileup). A GROWTH is safe on the steady
|
|
2278
|
-
// path (the new live's bottom-anchored extent covers the old — the
|
|
2279
|
-
// erased rows are all re-painted); a shrink takes the full-redraw
|
|
2280
|
-
// path: the window re-paints at the model's positions, every row
|
|
2281
|
-
// covered (the V6-1 every-row rule).
|
|
2282
|
-
// REL-0152-R1: ONE renderer. The steady/full split existed because
|
|
2283
|
-
// the steady path moved rows by scrolling and could not handle a
|
|
2284
|
-
// window that moved the wrong way, so the frames it could not draw
|
|
2285
|
-
// were handed to a full repaint. A diff has no such frames: it
|
|
2286
|
-
// emits the rows that differ, and on a frame where everything
|
|
2287
|
-
// differs that IS a full repaint. The dispatch, the two geometries
|
|
2288
|
-
// and the invariant about which moves each path may use all go
|
|
2289
|
-
// with it.
|
|
2290
|
-
this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1916
|
+
// REL-0152-R1: ONE renderer. A diff emits the rows that differ, and
|
|
1917
|
+
// on a frame where everything differs that IS a full repaint — so
|
|
1918
|
+
// the old steady/full split (a scrolling path that could not draw
|
|
1919
|
+
// a window moving the wrong way, finding #A8) has nothing left to
|
|
1920
|
+
// dispatch between.
|
|
1921
|
+
this.#drawFull(out, W, H, liveTop, liveLines, chrome.queueRows, chrome.menuRows, chrome.editor);
|
|
2291
1922
|
this.#fullRedraw = false;
|
|
2292
1923
|
// REL-0161: the frame close no longer shows the cursor — hidden IS
|
|
2293
1924
|
// the steady state (Terminal.app infers "a prompt line" from
|
|
@@ -2305,19 +1936,11 @@ export class Body {
|
|
|
2305
1936
|
}
|
|
2306
1937
|
this.#lastLiveTop = liveTop;
|
|
2307
1938
|
this.#lastLiveRows = liveRowsTotal;
|
|
2308
|
-
this.#lastInputRows = editor.rows.length;
|
|
1939
|
+
this.#lastInputRows = chrome.editor.rows.length;
|
|
2309
1940
|
// KC1 §6: the next steady frame's relative moves start where THIS
|
|
2310
1941
|
// frame's CHA parked the cursor — the marker's row inside the
|
|
2311
1942
|
// composer (N = 1 ⇒ H−2, the retired hard-coded anchor).
|
|
2312
|
-
this.#lastAnchorRow = H - 1 - editor.rows.length + editor.markerRow;
|
|
2313
|
-
}
|
|
2314
|
-
/** R13 D1 — the same constant as everything else. This used to compute
|
|
2315
|
-
* the lead from a ONE-ROW STAND-IN so the live block would claim the
|
|
2316
|
-
* spacing its fold was going to get; with the rhythm constant there
|
|
2317
|
-
* is nothing to simulate, and R7a's device retires with the formula
|
|
2318
|
-
* that needed it. */
|
|
2319
|
-
#blockSpace(i, prev, rows) {
|
|
2320
|
-
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
1943
|
+
this.#lastAnchorRow = H - 1 - chrome.editor.rows.length + chrome.editor.markerRow;
|
|
2321
1944
|
}
|
|
2322
1945
|
#space(i, prev, rows) {
|
|
2323
1946
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
@@ -2326,18 +1949,9 @@ export class Body {
|
|
|
2326
1949
|
// so a settle changes content and never position by construction.
|
|
2327
1950
|
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
2328
1951
|
}
|
|
2329
|
-
/**
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2332
|
-
* The spacing formula reads what stood above; a cell that rendered
|
|
2333
|
-
* nothing did not stand above anything. Since R3d whole families of
|
|
2334
|
-
* cells render `[]` — the members a fold speaks for — and the
|
|
2335
|
-
* formula was reading that empty array as "a zero-row sibling", so a
|
|
2336
|
-
* multi-row block following a fold lost the blank that belongs above
|
|
2337
|
-
* it. The defect predates this round (any folded turn followed by a
|
|
2338
|
-
* raw block has it); R3i's projection is what finally put a test on
|
|
2339
|
-
* the path.
|
|
2340
|
-
*/
|
|
1952
|
+
/** The previous DRAWN sibling, not the previous cell: the spacing
|
|
1953
|
+
* formula reads what stood above, and a cell that rendered nothing
|
|
1954
|
+
* did not stand above anything (R3i). */
|
|
2341
1955
|
#lastDrawn(i, prev) {
|
|
2342
1956
|
if (prev !== null && prev.length > 0)
|
|
2343
1957
|
return prev;
|
|
@@ -2356,238 +1970,36 @@ export class Body {
|
|
|
2356
1970
|
* this cell's commit — the cache stays raw, the placed rows count. */
|
|
2357
1971
|
#commitCell(i, W, ctx) {
|
|
2358
1972
|
const cell = this.#cells[i];
|
|
2359
|
-
const lines =
|
|
2360
|
-
// W15: a tool cell whose committed rows
|
|
2361
|
-
//
|
|
2362
|
-
//
|
|
2363
|
-
//
|
|
2364
|
-
//
|
|
2365
|
-
|
|
2366
|
-
// key does not answer would be the one thing worse than silence.
|
|
2367
|
-
// unshift: the cells commit oldest-first, so the NEWEST cut lands
|
|
2368
|
-
// at the front — the expand pointer's "newest back" walk starts
|
|
2369
|
-
// where the user's last key press would aim.
|
|
2370
|
-
// R3b: a fold HEAD joins the ring too. The test used to demand a
|
|
2371
|
-
// tool cell, and a segment's fold can be emitted at a thinking
|
|
2372
|
-
// cell — which would have left the whole segment unreachable by
|
|
2373
|
-
// the very key its own row advertises.
|
|
2374
|
-
// R4a — the ring captures by IDENTITY, not by searching our own
|
|
2375
|
-
// printed bytes.
|
|
2376
|
-
//
|
|
2377
|
-
// This used to require the rendered rows to contain the literal
|
|
2378
|
-
// "ctrl+o", which made the affordance LOAD-BEARING: retiring the
|
|
2379
|
-
// printed key (the owner's ruling) would have silently emptied the
|
|
2380
|
-
// ring and taken the expand key with it — not a missing hint, a
|
|
2381
|
-
// missing feature. A fold head is a fold head because the segment
|
|
2382
|
-
// says so; a tool cell is expandable when it is hiding rows.
|
|
2383
|
-
const isFoldHead = this.#segmentOf(i)?.headCell === i;
|
|
2384
|
-
const hidesRows = cell.kind === "tool" && lines.some((l) => l.includes("ctrl+o"));
|
|
2385
|
-
if (isFoldHead || hidesRows) {
|
|
1973
|
+
const lines = cellComponent(cell).render(W, ctx);
|
|
1974
|
+
// W15: a tool cell whose committed rows hide something (its rows
|
|
1975
|
+
// carry the "ctrl+o" affordance — the renderer's OWN output, so the
|
|
1976
|
+
// read's "/last"-only cut note never lands here) joins the collapsed
|
|
1977
|
+
// index the viewer reads (ctrl+r). unshift: cells commit
|
|
1978
|
+
// oldest-first, so the newest cut is at the front.
|
|
1979
|
+
if (cell.kind === "tool" && lines.some((l) => l.includes("ctrl+o")))
|
|
2386
1980
|
this.#collapsed.unshift(i);
|
|
2387
|
-
// R4a — a new fold resets the walk, so the FIRST press after any
|
|
2388
|
-
// new work always opens the most recent one. That is the whole
|
|
2389
|
-
// of the owner's "which one does it open": the answer is always
|
|
2390
|
-
// "the last one", and repeats walk back from there.
|
|
2391
|
-
this.#opened.clear();
|
|
2392
|
-
}
|
|
2393
1981
|
this.#lineCache[i] = lines;
|
|
2394
1982
|
const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
2395
1983
|
this.#committed += 1;
|
|
2396
1984
|
this.#committedLines += placed.length;
|
|
2397
1985
|
this.#committedLinesThisFrame.push(...placed);
|
|
2398
1986
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
* needs a human's eye. Law 1.3 makes the same call about marks — a
|
|
2416
|
-
* failure keeps its colour AND its words — and this is that rule at
|
|
2417
|
-
* the scale of a run.
|
|
2418
|
-
*
|
|
2419
|
-
* The cost, accepted: a turn that reads twenty files and hits one
|
|
2420
|
-
* denial keeps all twenty rows. The alternative is a screen that says
|
|
2421
|
-
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
2422
|
-
*/
|
|
2423
|
-
/**
|
|
2424
|
-
* R3i — the trouble the stretch met, as the line's own terms.
|
|
2425
|
-
*
|
|
2426
|
-
* Law 1.3: an outcome is stated in WORDS, "the only form that
|
|
2427
|
-
* survives a pipe". So the kind is a different word, never a
|
|
2428
|
-
* different colour — `2 failed`, `1 denied`, `1 interrupted` — and
|
|
2429
|
-
* the failure's identity rides with it. In this phase the terms are
|
|
2430
|
-
* only DRAWN (the live line names trouble the moment it happens);
|
|
2431
|
-
* whether trouble still blocks the fold is the next phase's ruling.
|
|
2432
|
-
*/
|
|
2433
|
-
/** R3i — is this cell one the fold must not count as work done? */
|
|
2434
|
-
#cellInTrouble(i) {
|
|
2435
|
-
const c = this.#cells[i];
|
|
2436
|
-
if (c === undefined || c.kind !== "tool")
|
|
2437
|
-
return false;
|
|
2438
|
-
return c.isError || c.reason !== null || c.verdict?.decision === "denied";
|
|
2439
|
-
}
|
|
2440
|
-
#segmentTroubleTerms(seg) {
|
|
2441
|
-
let failed = 0;
|
|
2442
|
-
let denied = 0;
|
|
2443
|
-
let interrupted = 0;
|
|
2444
|
-
let what = "";
|
|
2445
|
-
for (const j of seg.cells) {
|
|
2446
|
-
const c = this.#cells[j];
|
|
2447
|
-
if (c === undefined || c.kind !== "tool")
|
|
2448
|
-
continue;
|
|
2449
|
-
// WHICH call, and WHY. The target alone answers the first and
|
|
2450
|
-
// not the second, and for a policy denial the second is the
|
|
2451
|
-
// whole point: `sub/out.txt` does not tell a human that plan
|
|
2452
|
-
// mode is read-only, and that sentence is the one they act
|
|
2453
|
-
// on. Law 1.3's own words — an outcome is stated in words —
|
|
2454
|
-
// and the ladder cuts this clause last, so it degrades to the
|
|
2455
|
-
// target before it disappears.
|
|
2456
|
-
const named = () => {
|
|
2457
|
-
const t = toolTarget(c.name, JSON.parse(c.inputFull));
|
|
2458
|
-
const why = c.verdict?.reason ?? c.reason;
|
|
2459
|
-
return why === null || why === undefined || why === "" || why === "interrupted" ? t : `${t} (${why})`;
|
|
2460
|
-
};
|
|
2461
|
-
if (c.verdict?.decision === "denied" || (c.reason !== null && c.reason !== "interrupted" && /denied/i.test(c.reason))) {
|
|
2462
|
-
denied += 1;
|
|
2463
|
-
if (what === "")
|
|
2464
|
-
what = named();
|
|
2465
|
-
}
|
|
2466
|
-
else if (c.reason === "interrupted") {
|
|
2467
|
-
interrupted += 1;
|
|
2468
|
-
}
|
|
2469
|
-
else if (c.isError || c.reason !== null) {
|
|
2470
|
-
failed += 1;
|
|
2471
|
-
if (what === "")
|
|
2472
|
-
what = named();
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
const out = [];
|
|
2476
|
-
if (failed > 0)
|
|
2477
|
-
out.push(["failed", failed, what]);
|
|
2478
|
-
if (denied > 0)
|
|
2479
|
-
out.push(["denied", denied, what]);
|
|
2480
|
-
if (interrupted > 0)
|
|
2481
|
-
out.push(["interrupted", interrupted, ""]);
|
|
2482
|
-
return out;
|
|
2483
|
-
}
|
|
2484
|
-
#segmentHasTrouble(seg) {
|
|
2485
|
-
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
2486
|
-
// exists for, and it was the one case the predicate could not
|
|
2487
|
-
// see — a denial carrying no `reason` string leaves isError
|
|
2488
|
-
// false and reason null, so `✦ thought 3s · 20 reads` could
|
|
2489
|
-
// stand over a refused write. The verdict is the record of it.
|
|
2490
|
-
return this.#segmentTools(seg).some((c) => c.isError || c.reason !== null || c.verdict?.decision === "denied");
|
|
2491
|
-
}
|
|
2492
|
-
/** R3b — the segment's TOOL cells, in order. */
|
|
2493
|
-
#segmentTools(seg) {
|
|
2494
|
-
const out = [];
|
|
2495
|
-
for (const j of seg.cells) {
|
|
2496
|
-
const c = this.#cells[j];
|
|
2497
|
-
if (c.kind === "tool")
|
|
2498
|
-
out.push(c);
|
|
2499
|
-
}
|
|
2500
|
-
return out;
|
|
2501
|
-
}
|
|
2502
|
-
/** R3f — the cell is leaving the live region under the screen's hard
|
|
2503
|
-
* cap, so its segment can no longer be represented by a fold. */
|
|
2504
|
-
#markSpilled(i) {
|
|
2505
|
-
const seg = this.#segmentOf(i);
|
|
2506
|
-
if (seg !== null)
|
|
2507
|
-
seg.spilled = true;
|
|
2508
|
-
}
|
|
2509
|
-
/** R3b — the segment a committed cell belongs to, or null when it has
|
|
2510
|
-
* none (a cell of the pipe path, or a kind that is not work). */
|
|
2511
|
-
#segmentOf(i) {
|
|
2512
|
-
const cell = this.#cells[i];
|
|
2513
|
-
if (cell.kind !== "thinking" && cell.kind !== "tool")
|
|
2514
|
-
return null;
|
|
2515
|
-
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2516
|
-
const si = this.#cellSegment[i];
|
|
2517
|
-
if (turn === undefined || si === undefined || si < 0)
|
|
2518
|
-
return null;
|
|
2519
|
-
return turn.segments[si] ?? null;
|
|
2520
|
-
}
|
|
2521
|
-
/** W14 — the fold-hold: a thinking/tool cell of the OPEN quiet turn
|
|
2522
|
-
* (no text yet) does not commit — its committed form is decided at
|
|
2523
|
-
* the release. The cell's OWN turn must be the CURRENT one (a cell
|
|
2524
|
-
* of a released turn commits normally). The force-commit path never
|
|
2525
|
-
* consults this — the screen's hard cap wins over the hold. */
|
|
2526
|
-
#held(i) {
|
|
2527
|
-
// R13 — NOTHING IS HELD ANY MORE. DECLARED REVERSAL of W14's
|
|
2528
|
-
// quiet-turn hold, R3b/R3i's segment hold and TUI2-R1.5 ①'s
|
|
2529
|
-
// explore-run hold, all three of them owner-ruled away on
|
|
2530
|
-
// 2026-09-03 with the mechanism they served.
|
|
2531
|
-
//
|
|
2532
|
-
// Every one of them existed for the same reason: a done cell's
|
|
2533
|
-
// COMMITTED FORM was undecided while its segment or its run was
|
|
2534
|
-
// still open, because a fold line or a rollup row might yet stand
|
|
2535
|
-
// for it — and a committed row cannot be taken back (ADR-0046).
|
|
2536
|
-
// With no fold and no rollup, a call's committed form is its own
|
|
2537
|
-
// card and is settled the instant the call is: there is nothing
|
|
2538
|
-
// left to wait for.
|
|
2539
|
-
//
|
|
2540
|
-
// It is not only dead weight. Holding done cells in the live
|
|
2541
|
-
// region made the region carry work that was FINISHED, so a burst
|
|
2542
|
-
// of four reads and a shell in flight competed for the same rows
|
|
2543
|
-
// and DC-43's shrink took the running call's output away — the
|
|
2544
|
-
// one thing on the screen the human is waiting for (R7a D). Let
|
|
2545
|
-
// them commit and the room is there.
|
|
2546
|
-
void i;
|
|
2547
|
-
return false;
|
|
2548
|
-
}
|
|
2549
|
-
/* R13 — `#growingRun` retired with the rollup's commit hold (#held). */
|
|
2550
|
-
/** W14/W13 — the release-time decision at a commit, BEFORE the cell's
|
|
2551
|
-
* own render: the folded-turn fold first (a QUIET turn — ended, no
|
|
2552
|
-
* text — becomes the ONE fold line; the rest of its thinking/tool
|
|
2553
|
-
* cells render [] after the fold), then the W13 rollup (a text
|
|
2554
|
-
* turn's N > 2 same-tool run: the HEAD renders the group summary,
|
|
2555
|
-
* the members render [] — the scan is the work order's "group key",
|
|
2556
|
-
* derived at commit time, never pre-stored). */
|
|
2557
|
-
#foldOrRollup(cell, i, W, ctx) {
|
|
2558
|
-
// R13 — DECLARED REVERSAL, three at once, all of them the same
|
|
2559
|
-
// idea and all of them owner-ruled on 2026-09-03:
|
|
2560
|
-
//
|
|
2561
|
-
// · the SEGMENT FOLD (R3b–R3i, W14) — a closed stretch of two or
|
|
2562
|
-
// more cells collapsed into one settled line;
|
|
2563
|
-
// · the W13 ROLLUP and the `rolled` cell field — a run of three
|
|
2564
|
-
// or more same-tool calls collapsed into a group summary;
|
|
2565
|
-
// · TUI2-R1 (B)'s EXPLORATION ROW — the mixed read-only variant
|
|
2566
|
-
// of the same collapse.
|
|
2567
|
-
//
|
|
2568
|
-
// Every one of them answered the same pressure: ungrounded output
|
|
2569
|
-
// rows owned the screen, so work was collapsed into sentences
|
|
2570
|
-
// ABOUT the work. The card is what changes that arithmetic — a
|
|
2571
|
-
// call's rows sit inside a surface that says where it begins and
|
|
2572
|
-
// ends, so five of them read as one object rather than five loose
|
|
2573
|
-
// lines — and the owner's ruling is that the collapse costs more
|
|
2574
|
-
// than it buys: a page where the machine's work is sometimes a
|
|
2575
|
-
// card, sometimes a summary line and sometimes nothing at all is
|
|
2576
|
-
// a page a reader cannot predict. One rhythm, one surface.
|
|
2577
|
-
//
|
|
2578
|
-
// So every cell renders itself, and this method is the record of
|
|
2579
|
-
// what used to happen here. What the folds bought is bought
|
|
2580
|
-
// differently now: the preview cap (five rows a call, E1's read
|
|
2581
|
-
// showing none) is what keeps a burst from owning the screen, and
|
|
2582
|
-
// it is a CONSTANT per call rather than a decision about runs.
|
|
2583
|
-
//
|
|
2584
|
-
// What goes with them: `#growingRun`'s commit hold existed only so
|
|
2585
|
-
// a run's committed FORM could be decided once the run closed —
|
|
2586
|
-
// with no run-level form left to decide, a done cell commits when
|
|
2587
|
-
// it is done. `#rolledHeads`, `rolledOf`, `rolledTitle`,
|
|
2588
|
-
// `rolledDetail` and the `rolled` field go with the rollup.
|
|
2589
|
-
return cellComponent(cell).render(W, ctx);
|
|
2590
|
-
}
|
|
1987
|
+
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): the segment
|
|
1988
|
+
bookkeeping stood here — `#stampSegment`, `#segmentOf`,
|
|
1989
|
+
`#segmentHasTrouble`, `#segmentTroubleTerms`, `#markSpilled` — and
|
|
1990
|
+
with it the three commit HOLDS (`#held`: W14's quiet turn, R3b/R3i's
|
|
1991
|
+
segment, TUI2-R1.5 ①'s explore run). All of it decided whether a
|
|
1992
|
+
fold or a rollup would stand for a done cell. Nothing folds, so a
|
|
1993
|
+
done cell commits the instant it is done, and holding it back only
|
|
1994
|
+
made finished work compete with the running call for rows (R7a D). */
|
|
1995
|
+
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): `#foldOrRollup`
|
|
1996
|
+
stood here — the commit-time decision whether a cell rendered
|
|
1997
|
+
itself, a segment's fold line (R3b–R3i, W14), a W13 rollup summary
|
|
1998
|
+
or TUI2-R1 (B)'s exploration row. All three collapses answered one
|
|
1999
|
+
pressure (ungrounded output rows owning the screen) by turning work
|
|
2000
|
+
into sentences ABOUT work; the card answers it with a constant
|
|
2001
|
+
preview cap per call, and a page whose work is always the same
|
|
2002
|
+
shape is one a reader can predict. Every cell renders itself. */
|
|
2591
2003
|
/** The slot occupant's extra rows — the slash-command menu (above the
|
|
2592
2004
|
* status, in the rhythm gap + the content's spare rows — the old
|
|
2593
2005
|
* menu's position, slot-shaped). */
|
|
@@ -2944,38 +2356,30 @@ export class Body {
|
|
|
2944
2356
|
// sit with the live band empty and the chrome at its minimum — the
|
|
2945
2357
|
// one-way part of a movement that otherwise goes both ways.
|
|
2946
2358
|
if (this.#resizeFrame) {
|
|
2947
|
-
//
|
|
2948
|
-
// window is the terminal's own scroll — it reflows the old
|
|
2949
|
-
// content and pushes the overflow into its scrollback before we
|
|
2950
|
-
// are called — so emitting our own LFs on top put the same rows
|
|
2951
|
-
// in twice (TT-1B: twelve rows of a forty-line burst). The
|
|
2952
|
-
// counter adopts the terminal's work, and the held screen is
|
|
2953
|
-
// discarded because a reflow invalidates every row of it: the
|
|
2954
|
-
// next diff repaints the whole screen, which is exactly what a
|
|
2955
|
-
// resize needs.
|
|
2956
|
-
// DC-34 — NO HIGH-WATER MARK ON A RESIZE.
|
|
2359
|
+
// R14 / route B — THE RESIZE FRAME SCROLLS LIKE ANY OTHER.
|
|
2957
2360
|
//
|
|
2958
|
-
//
|
|
2959
|
-
//
|
|
2960
|
-
//
|
|
2961
|
-
//
|
|
2962
|
-
//
|
|
2361
|
+
// Everything this branch used to hold is retired with the
|
|
2362
|
+
// window arithmetic it served. It adopted the terminal's own
|
|
2363
|
+
// reflow-scroll into `#scrolledOff` (REL-0152-R1), then had to
|
|
2364
|
+
// stop adopting it on a widen and keep adopting it on a narrow
|
|
2365
|
+
// (DC-34), because the counter it was feeding meant a different
|
|
2366
|
+
// thing at every fold width. There is no such counter to feed
|
|
2367
|
+
// now: `#settleResize` erased the terminal and set
|
|
2368
|
+
// `#scrolledOff` to zero, so the frontier is real and empty and
|
|
2369
|
+
// the frame simply reprints from the top.
|
|
2963
2370
|
//
|
|
2964
|
-
//
|
|
2965
|
-
//
|
|
2966
|
-
//
|
|
2967
|
-
//
|
|
2968
|
-
//
|
|
2969
|
-
//
|
|
2970
|
-
//
|
|
2971
|
-
//
|
|
2972
|
-
if (this.#refolded)
|
|
2973
|
-
this.#scrolledOff = Math.max(this.#scrolledOff, Math.max(0, Math.min(skip, all.length)));
|
|
2974
|
-
this.#refolded = false;
|
|
2371
|
+
// Falling through to `#emitScroll` is not a convenience — it is
|
|
2372
|
+
// the whole point. The transcript above the last screenful has
|
|
2373
|
+
// to reach the scrollback, chunked and staged from the model,
|
|
2374
|
+
// exactly as a resumed session's replay does. Without it the
|
|
2375
|
+
// reprint painted the last screenful onto a terminal whose
|
|
2376
|
+
// history it had just erased, and everything above was simply
|
|
2377
|
+
// GONE: `dc34-widen-seam` measured 36 tokens missing from the
|
|
2378
|
+
// scrollback the first time this branch was left in place.
|
|
2975
2379
|
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
2976
2380
|
this.#resizeFrame = false;
|
|
2977
2381
|
}
|
|
2978
|
-
|
|
2382
|
+
if (!overlay) {
|
|
2979
2383
|
// DC-46 — THE ROWS THAT LEAVE THE WINDOW SCROLL, and the target
|
|
2980
2384
|
// is `skip` itself.
|
|
2981
2385
|
//
|
|
@@ -3009,26 +2413,12 @@ export class Body {
|
|
|
3009
2413
|
// OLDEST on screen — they are still in the model and come back on
|
|
3010
2414
|
// the close.
|
|
3011
2415
|
const contentRows = Math.max(0, H - CHROME_ROWS - inputExtra - queueRows.length - menuRows.length);
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
//
|
|
3015
|
-
//
|
|
3016
|
-
//
|
|
3017
|
-
|
|
3018
|
-
// cells above the frontier, or the live band collapsing — and
|
|
3019
|
-
// nothing stopped it (rider 3's ungated reach-back).
|
|
3020
|
-
//
|
|
3021
|
-
// Clamping costs a gap under short content for one frame, which
|
|
3022
|
-
// the next commit fills. Reaching back costs a duplicate that
|
|
3023
|
-
// stands in the transcript forever.
|
|
3024
|
-
// ...but only when the WIDTH moved. A height change re-indexes
|
|
3025
|
-
// nothing — the folds are untouched, every row means what it
|
|
3026
|
-
// meant — so reaching back there is the pre-existing behaviour a
|
|
3027
|
-
// gate already covers (the A8 windowing case: grow the screen and
|
|
3028
|
-
// the banner returns). The duplication measured in this round is
|
|
3029
|
-
// width-driven, and so is the guard.
|
|
3030
|
-
const march = all.slice(this.#lastW !== 0 && this.#lastW !== W ? Math.max(skip, this.#scrolledOff) : skip);
|
|
3031
|
-
this.#lastW = W;
|
|
2416
|
+
// The march starts at `skip`, which is already clamped to the
|
|
2417
|
+
// frontier above (rows in [0, #scrolledOff) are the terminal's and
|
|
2418
|
+
// are never painted again). DC-34's width-only reach-back guard
|
|
2419
|
+
// stood here until R14: a resize now reprints from the model, so
|
|
2420
|
+
// there is no stale fold for a width change to re-index.
|
|
2421
|
+
const march = all.slice(skip);
|
|
3032
2422
|
for (const line of march.length > contentRows ? march.slice(march.length - contentRows) : march) {
|
|
3033
2423
|
desired[r - 1] = this.#checked(line, W);
|
|
3034
2424
|
r += 1;
|
|
@@ -3052,7 +2442,7 @@ export class Body {
|
|
|
3052
2442
|
desired[H - 2 - inputExtra + i - 1] = this.#checked(editor.rows[i], W);
|
|
3053
2443
|
desired[H - 1 - 1] = boxBottom(W);
|
|
3054
2444
|
const statusRow = this.#statusSource();
|
|
3055
|
-
desired[H - 1] = this.#checked(statusLine(statusRow.status, this.#tail, W, statusRow.hint), W);
|
|
2445
|
+
desired[H - 1] = this.#checked(statusLine(statusRow.status, this.#tail, W, statusRow.hint, statusRow.expand), W);
|
|
3056
2446
|
this.#emitDiff(out, W, H, desired);
|
|
3057
2447
|
// REL-0152-R1: park from where the cursor ACTUALLY is — see
|
|
3058
2448
|
// #cursorRow. It used to be parked from H, which the bottom-up
|