@vincemakes/kiso-tui 0.25.0 → 0.26.1
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 +22 -42
- package/dist/compositor.js +265 -924
- 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, Container,
|
|
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,22 +163,6 @@ export class Body {
|
|
|
198
163
|
#needsReset = false;
|
|
199
164
|
#resizeTimer = null;
|
|
200
165
|
#lastH = 0;
|
|
201
|
-
/** DC-34 — did THIS FRAME refold the committed cells?
|
|
202
|
-
*
|
|
203
|
-
* It must be reset where the question is asked, not only where it
|
|
204
|
-
* is answered. Armed once and consumed later, it latched: the
|
|
205
|
-
* session's FIRST frame ran a vacuous refold over zero committed
|
|
206
|
-
* cells and set it, and nothing cleared it until the first resize —
|
|
207
|
-
* so every session's first widen still ran the adopt it was
|
|
208
|
-
* supposed to skip, and swallowed the live band's worth of
|
|
209
|
-
* committed rows. Three paragraphs, in the measurement that found
|
|
210
|
-
* it. */
|
|
211
|
-
#refolded = false;
|
|
212
|
-
/** DC-34 — the previous frame's width; the reach-back guard is for a
|
|
213
|
-
* WIDTH change, which re-indexes the model, not a height change. */
|
|
214
|
-
#lastW = 0;
|
|
215
|
-
/** R14 — the geometry the last SETTLE ran at. A winch back to the
|
|
216
|
-
* same size is not a resize and must not erase the scrollback. */
|
|
217
166
|
/** R14 — the geometry the last frame PAINTED, and the snapshot of it
|
|
218
167
|
* taken when a winch opens a settle window.
|
|
219
168
|
*
|
|
@@ -248,11 +197,6 @@ export class Body {
|
|
|
248
197
|
#lastThinking = null;
|
|
249
198
|
#lastTool = null;
|
|
250
199
|
#pendingCalls = new Map();
|
|
251
|
-
/** R3b — cell index → the index of the segment it belongs to, for
|
|
252
|
-
* thinking/tool cells; -1 for every other kind. Parallel to #cells,
|
|
253
|
-
* because a segment is the COMPOSITOR's bookkeeping and does not
|
|
254
|
-
* belong on the cell type the renderer sees. */
|
|
255
|
-
#cellSegment = [];
|
|
256
200
|
#pipeBuf = ""; // the passthrough's thinking buffer
|
|
257
201
|
/** TUI2-MD ⑤ — the markdown scanner of the message currently
|
|
258
202
|
* streaming, and the cell index its first block landed at. Null
|
|
@@ -267,14 +211,9 @@ export class Body {
|
|
|
267
211
|
// VIEWER's index now (ctrl+r, `#viewerEntries`) — the two surfaces
|
|
268
212
|
// coexist and §9 says so. It is kept, not retired, for that reader.
|
|
269
213
|
#collapsed = [];
|
|
270
|
-
//
|
|
271
|
-
// machine (ended / hasText / folded) plus the folded-turn line's
|
|
272
|
-
// counts (accumulated at toolStart). The cells carry the record's
|
|
214
|
+
// the turn records — one per userLine; the cells carry the record's
|
|
273
215
|
// index as their turn boundary.
|
|
274
216
|
#turns = [];
|
|
275
|
-
// W13: the rolled-up run heads — the commit-time scan's verdict:
|
|
276
|
-
// the head's group summary renders, the members render [].
|
|
277
|
-
#rolledHeads = new Set();
|
|
278
217
|
#write;
|
|
279
218
|
#resizeHandler = null;
|
|
280
219
|
/** TUI2-R3v2 ②: the panel option rows' absolute screen span, as of the
|
|
@@ -414,10 +353,8 @@ export class Body {
|
|
|
414
353
|
}
|
|
415
354
|
this.#closeOpenThinking();
|
|
416
355
|
this.#closeOpenText();
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
// the user's own words ride the record — the fold's leading chip.
|
|
420
|
-
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 });
|
|
421
358
|
this.#cells.push({ kind: "user", text, done: true, turn: this.#turns.length - 1 });
|
|
422
359
|
this.#mark();
|
|
423
360
|
}
|
|
@@ -432,42 +369,15 @@ export class Body {
|
|
|
432
369
|
}
|
|
433
370
|
else {
|
|
434
371
|
this.#cells.push({ kind: "thinking", text, done: false, turn: this.#turns.length - 1 });
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// R3b made thinking open a segment, on the reading that it is
|
|
439
|
-
// work like a tool call. Four rounds of consequences followed
|
|
440
|
-
// from that one classification: folded away with the calls, it
|
|
441
|
-
// became unreachable, and R4's printed ordinal, R5's viewer,
|
|
442
|
-
// R6's subject index and a look-back viewport were each built
|
|
443
|
-
// to hand it back. The owner's ruling is to stop hiding it —
|
|
444
|
-
// and then none of those mechanisms is answering a question
|
|
445
|
-
// anyone still asks.
|
|
446
|
-
//
|
|
447
|
-
// So thinking CLOSES the open segment, exactly as text does
|
|
448
|
-
// (see textAppend): a segment is what sits between two of
|
|
449
|
-
// these. It must close rather than merely not-open, because
|
|
450
|
-
// `#committed` is a PREFIX count — a thinking cell cannot
|
|
451
|
-
// commit past a held call, so think → call → think would
|
|
452
|
-
// otherwise flush at the segment's close with the second
|
|
453
|
-
// thought printing BELOW the fold that contains the later
|
|
454
|
-
// call.
|
|
455
|
-
//
|
|
456
|
-
// Consequence, and it is wanted: the segment's thinking clock
|
|
457
|
-
// never starts, so `thought Ns` drops off every fold line by
|
|
458
|
-
// R3h's own zero-term rule. The line stops claiming a fact the
|
|
459
|
-
// paragraph above it already states in full.
|
|
372
|
+
// R7 (owner-ruled 2026-08-31): thinking is WORDS, not work — a
|
|
373
|
+
// cell like prose.
|
|
460
374
|
const t0 = this.#turns[this.#turns.length - 1];
|
|
461
|
-
closeSegment(t0, Date.now());
|
|
462
375
|
if (t0 !== undefined)
|
|
463
|
-
t0.begun = true;
|
|
464
|
-
// R3i:
|
|
465
|
-
// twinkles", and `#armSpinner`'s
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// never moved at all. The line's seconds are a frame-time
|
|
469
|
-
// derivation, so without the beat they also never ticked: the
|
|
470
|
-
// 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.
|
|
471
381
|
this.#armSpinner();
|
|
472
382
|
}
|
|
473
383
|
this.#mark();
|
|
@@ -475,10 +385,6 @@ export class Body {
|
|
|
475
385
|
thinkingEnd() {
|
|
476
386
|
const last = this.#cells[this.#cells.length - 1];
|
|
477
387
|
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
478
|
-
// R3i: every closer — text, a notice, a terminal label, the next
|
|
479
|
-
// turn — routes through here, so the clock cannot keep running
|
|
480
|
-
// past the thing that ended it.
|
|
481
|
-
stopThinking(this.#turns[this.#turns.length - 1]?.segments.at(-1), Date.now());
|
|
482
388
|
last.done = true;
|
|
483
389
|
this.#lastThinking = last.text;
|
|
484
390
|
if (!this.#isActive())
|
|
@@ -503,9 +409,7 @@ export class Body {
|
|
|
503
409
|
// `done` flag says otherwise. The commit loop takes leading DONE
|
|
504
410
|
// cells, so that one stale flag parked the whole rest of the turn
|
|
505
411
|
// behind it — every tool cell then reached the screen through the
|
|
506
|
-
// FORCE-commit path
|
|
507
|
-
// why the walkthrough saw nine individual rows: not a fold that
|
|
508
|
-
// declined to form, a fold that was never consulted.
|
|
412
|
+
// FORCE-commit path.
|
|
509
413
|
this.#closeOpenThinking();
|
|
510
414
|
this.#closeOpenText();
|
|
511
415
|
// W12: the cell carries the delegate's child roles from the FULL
|
|
@@ -520,73 +424,11 @@ export class Body {
|
|
|
520
424
|
}
|
|
521
425
|
this.#toolCells.set(callId, this.#cells.length);
|
|
522
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 });
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
// 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.
|
|
526
429
|
const turn = this.#turns[this.#turns.length - 1];
|
|
527
|
-
if (turn !== undefined)
|
|
528
|
-
|
|
529
|
-
// distinct thing, not the act. Reading one file twice used to
|
|
530
|
-
// fold as `read 2 files` — a sentence law 1.3 forbids, and one
|
|
531
|
-
// this product shipped. `bump` is false on the second sighting
|
|
532
|
-
// of a target the term has already counted; an ACT-counting
|
|
533
|
-
// tool (a search, a shell command) always bumps, because two
|
|
534
|
-
// searches for the same pattern really are two searches.
|
|
535
|
-
// R3i phase 5 — an ANSWER is words, and words do not fold (law
|
|
536
|
-
// 1.7). `ask_user` closes the open stretch exactly as prose
|
|
537
|
-
// does, and never joins one: absorbed into `1 × ask_user`,
|
|
538
|
-
// what the human said would be gone from the screen — and the
|
|
539
|
-
// one thing a summary must not do is speak for the human.
|
|
540
|
-
if (name === "ask_user") {
|
|
541
|
-
// no stamp: it belongs to NO stretch, so no fold can speak
|
|
542
|
-
// for it — the same standing a block of prose has.
|
|
543
|
-
closeSegment(turn, Date.now());
|
|
544
|
-
this.#mark();
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
548
|
-
const bump = (rec) => {
|
|
549
|
-
if (target === null)
|
|
550
|
-
return true;
|
|
551
|
-
let set = rec.seen.get(name);
|
|
552
|
-
if (set === undefined) {
|
|
553
|
-
set = new Set();
|
|
554
|
-
rec.seen.set(name, set);
|
|
555
|
-
}
|
|
556
|
-
if (set.has(target))
|
|
557
|
-
return false;
|
|
558
|
-
set.add(target);
|
|
559
|
-
return true;
|
|
560
|
-
};
|
|
561
|
-
if (bump(turn)) {
|
|
562
|
-
if (name === "read_file")
|
|
563
|
-
turn.reads += 1;
|
|
564
|
-
else if (name === "edit_file")
|
|
565
|
-
turn.edits += 1;
|
|
566
|
-
else
|
|
567
|
-
turn.others.set(name, (turn.others.get(name) ?? 0) + 1);
|
|
568
|
-
}
|
|
569
|
-
// R3b: and into the SEGMENT, which opens here when this is the
|
|
570
|
-
// first work since the last text block. Its set is its OWN — a
|
|
571
|
-
// file read once per segment is one file in each segment's
|
|
572
|
-
// terms and one file in the turn's.
|
|
573
|
-
const seg = openSegment(turn, Date.now());
|
|
574
|
-
// R3i: a tool call is a NON-thinking event — the clock stops,
|
|
575
|
-
// exactly as the CLI's does at the same boundary.
|
|
576
|
-
stopThinking(seg ?? undefined, Date.now());
|
|
577
|
-
if (seg !== null && bump(seg)) {
|
|
578
|
-
if (name === "read_file")
|
|
579
|
-
seg.reads += 1;
|
|
580
|
-
else if (name === "edit_file")
|
|
581
|
-
seg.edits += 1;
|
|
582
|
-
else
|
|
583
|
-
seg.others.set(name, (seg.others.get(name) ?? 0) + 1);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
this.#stampSegment();
|
|
587
|
-
const t1 = this.#turns[this.#turns.length - 1];
|
|
588
|
-
if (t1 !== undefined)
|
|
589
|
-
t1.begun = true; // R6/D1: the block allocates here
|
|
430
|
+
if (turn !== undefined && name !== "ask_user")
|
|
431
|
+
turn.begun = true;
|
|
590
432
|
this.#mark();
|
|
591
433
|
}
|
|
592
434
|
toolApproval(callId, diff) {
|
|
@@ -702,17 +544,9 @@ export class Body {
|
|
|
702
544
|
this.#write(escapeTerminal(text));
|
|
703
545
|
return;
|
|
704
546
|
}
|
|
705
|
-
// W14: the text's arrival RELEASES the fold-hold — the turn now
|
|
706
|
-
// has text, its held cells commit individually (with the W13
|
|
707
|
-
// rollups; the fold is only for the QUIET turn).
|
|
708
547
|
const turn = this.#turns[this.#turns.length - 1];
|
|
709
548
|
if (turn !== undefined)
|
|
710
549
|
turn.hasText = true;
|
|
711
|
-
// R3b: text CLOSES the open segment. This is the boundary design.md
|
|
712
|
-
// §8 names — "folding at every text boundary changes what commits
|
|
713
|
-
// and when" — and it is the whole mechanism: a segment is what sits
|
|
714
|
-
// between two of these.
|
|
715
|
-
closeSegment(turn, Date.now());
|
|
716
550
|
// TUI2-MD ⑤: assistant body text is MARKDOWN, scanned as it
|
|
717
551
|
// streams. The scanner yields CLOSED blocks (final source, final
|
|
718
552
|
// render) and one OPEN tail block; each becomes a cell, and the
|
|
@@ -780,14 +614,9 @@ export class Body {
|
|
|
780
614
|
this.#endMd();
|
|
781
615
|
this.#mark();
|
|
782
616
|
}
|
|
783
|
-
/**
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
* the CLI's wall-clocked thinking window. The QUIET turn (ended, no
|
|
787
|
-
* text) releases its held cells as the ONE fold line; a turn with
|
|
788
|
-
* text releases them as individual commits (the W13 rollups). The
|
|
789
|
-
* release is LAZY — the held cells commit at the next frame, when
|
|
790
|
-
* 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. */
|
|
791
620
|
endTurn(thoughtSeconds) {
|
|
792
621
|
if (!this.#isActive())
|
|
793
622
|
return;
|
|
@@ -796,9 +625,6 @@ export class Body {
|
|
|
796
625
|
return;
|
|
797
626
|
turn.ended = true;
|
|
798
627
|
turn.thoughtSeconds = thoughtSeconds;
|
|
799
|
-
// R3b: the settle closes the last open segment — the turn's end is
|
|
800
|
-
// a boundary exactly as a text block is.
|
|
801
|
-
closeSegment(turn, Date.now());
|
|
802
628
|
// W20: the turn's live task block settles HERE — the ONE recap
|
|
803
629
|
// block for the turn ("`task done · N items · <duration>", the
|
|
804
630
|
// duration clocked compositor-side from the block's first call —
|
|
@@ -821,9 +647,7 @@ export class Body {
|
|
|
821
647
|
// and only ever left it through the force-commit cap. The turn's
|
|
822
648
|
// end is the boundary that closes them, exactly as it closes an
|
|
823
649
|
// open thinking cell. `reason` is set so the row keeps its words
|
|
824
|
-
//
|
|
825
|
-
// interruption is trouble, and law 1.3 says trouble is never
|
|
826
|
-
// summarised away.
|
|
650
|
+
// (law 1.3: trouble is never summarised away).
|
|
827
651
|
for (const c of this.#cells) {
|
|
828
652
|
if (c.kind === "tool" && !c.done) {
|
|
829
653
|
c.state = "done";
|
|
@@ -833,10 +657,8 @@ export class Body {
|
|
|
833
657
|
}
|
|
834
658
|
}
|
|
835
659
|
// the QUIET turn: an open thinking cell closes at the boundary —
|
|
836
|
-
// its natural closer is the text's arrival
|
|
837
|
-
//
|
|
838
|
-
// it (the commit loop only takes done cells — the fold would stall
|
|
839
|
-
// 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.
|
|
840
662
|
for (let i = this.#cells.length - 1; i >= 0; i -= 1) {
|
|
841
663
|
const c = this.#cells[i];
|
|
842
664
|
if (c.kind === "thinking" && !c.done) {
|
|
@@ -964,68 +786,24 @@ export class Body {
|
|
|
964
786
|
lastTool() {
|
|
965
787
|
return this.#lastTool;
|
|
966
788
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
* block at the bottom instead, the /last idiom aimed at a chosen
|
|
973
|
-
* cell: the pointer cycles the collapsed history, newest first, and
|
|
974
|
-
* the header names the target ("N turns back" — the user cells
|
|
975
|
-
* after it), so every press tells the user what they got. */
|
|
976
|
-
/**
|
|
977
|
-
* TUI2-R2 ⑤ — the cell the next ctrl+o will act on, or -1.
|
|
978
|
-
*
|
|
979
|
-
* The rule is expandNext's own first loop, extracted verbatim: the
|
|
980
|
-
* LAST live cell that can toggle. It is a separate method rather than
|
|
981
|
-
* a shared constant because the marker and the key must not merely
|
|
982
|
-
* agree today — the marker is a PROMISE about what the key will do,
|
|
983
|
-
* and the only way to keep it is to derive it from the same scan.
|
|
984
|
-
*
|
|
985
|
-
* The committed fallback (the #collapsed ring) is deliberately NOT
|
|
986
|
-
* marked: those rows are frozen history, never re-emitted, so a tint
|
|
987
|
-
* on them could not be moved when the pointer advances. A live target
|
|
988
|
-
* is the one the marker can tell the truth about.
|
|
989
|
-
*/
|
|
990
|
-
#focusIndex() {
|
|
991
|
-
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
992
|
-
const cell = this.#cells[i];
|
|
993
|
-
if (cell.kind === "tool" && cell.state !== "pending")
|
|
994
|
-
return i;
|
|
995
|
-
if (cell.kind === "checklist" && !cell.done)
|
|
996
|
-
return i;
|
|
997
|
-
}
|
|
998
|
-
return -1;
|
|
999
|
-
}
|
|
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). */
|
|
1000
794
|
/**
|
|
1001
|
-
* R4 (C4d) —
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
* line the terminal itself wrapped as the cursor flowed past the last
|
|
1010
|
-
* column. Every row kiso commits is either painted by cursor
|
|
1011
|
-
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
1012
|
-
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
1013
|
-
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
1014
|
-
* application hard-split. That same LF is what makes the transcript
|
|
1015
|
-
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
1016
|
-
* product whose transcript reflows is a product that repaints its
|
|
1017
|
-
* transcript from its own memory, and that transcript dies with it.
|
|
1018
|
-
*
|
|
1019
|
-
* What kiso can do — and this is all it can do — is APPEND. The
|
|
1020
|
-
* committed cells are still in memory; re-render them at the current
|
|
1021
|
-
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
1022
|
-
* 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.
|
|
1023
803
|
*
|
|
1024
804
|
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
1025
805
|
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
1026
|
-
*
|
|
1027
|
-
* width ladders, and re-printing them would duplicate work the folds
|
|
1028
|
-
* exist to state once.
|
|
806
|
+
* Cards and chips are short and carry their own width ladders.
|
|
1029
807
|
*/
|
|
1030
808
|
rewrap() {
|
|
1031
809
|
const W = this.#opts.width();
|
|
@@ -1152,8 +930,7 @@ export class Body {
|
|
|
1152
930
|
continue;
|
|
1153
931
|
// the tool card's FULL body — the same rows its own ctrl+o
|
|
1154
932
|
// opens. The expanded flag is saved and restored inside this
|
|
1155
|
-
// synchronous call
|
|
1156
|
-
// used for head.rolled; it never outlives the render, so the
|
|
933
|
+
// synchronous call; it never outlives the render, so the
|
|
1157
934
|
// committed geometry #committedLines derives can never see it.
|
|
1158
935
|
const saved = cell.expanded;
|
|
1159
936
|
cell.expanded = true;
|
|
@@ -1185,32 +962,13 @@ export class Body {
|
|
|
1185
962
|
cutLine(`${p.dim} ${viewerHint(this.#viewer, entries)}${p.reset}`, W),
|
|
1186
963
|
];
|
|
1187
964
|
}
|
|
1188
|
-
/*
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
* to choose which card came next, a set to stop the ring repeating
|
|
1196
|
-
* itself, and DC-35's guard to stop a HELD key printing the same four
|
|
1197
|
-
* rows three times — a defect the owner met in the field, each copy
|
|
1198
|
-
* closing with a footer that named something which did not exist.
|
|
1199
|
-
*
|
|
1200
|
-
* Amendment 1 removes the constraint. The terminal's scrollback is
|
|
1201
|
-
* ours to erase, so a committed card is re-rendered where it stands:
|
|
1202
|
-
* nothing to append, nothing to choose between, no repeat to guard.
|
|
1203
|
-
* `toggleExpanded` is the whole of the feature now.
|
|
1204
|
-
*
|
|
1205
|
-
* `#collapsed` STAYS — the viewer (ctrl+r) reads it as its index of
|
|
1206
|
-
* expandable cells, and §9 keeps the two surfaces side by side.
|
|
1207
|
-
*
|
|
1208
|
-
* `expandedCard` does NOT stay, and an earlier draft of this note said
|
|
1209
|
-
* it did. An expanded card is drawn by the ORDINARY card renderer with
|
|
1210
|
-
* `expanded` set — full body, `ctrl+o collapses` in place of the cut
|
|
1211
|
-
* note — which `r14-global-expand` pins. `expandedCard` was the shape
|
|
1212
|
-
* of the APPENDED block and has no caller once the append is gone.
|
|
1213
|
-
*/
|
|
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. */
|
|
1214
972
|
// ---- the Dock façade (the CLI's chrome API — same shape as the old dock) ----
|
|
1215
973
|
/** Docked = the chrome is live (a color TTY with a real size). */
|
|
1216
974
|
get active() {
|
|
@@ -1545,7 +1303,7 @@ export class Body {
|
|
|
1545
1303
|
// The terminal now holds nothing, so every record of what it held
|
|
1546
1304
|
// is void. `#scrolledOff` is the frontier of what reached its
|
|
1547
1305
|
// scrollback: after the erase, that is zero — which is also what
|
|
1548
|
-
// releases the committed cells to be
|
|
1306
|
+
// releases the committed cells to be re-rendered at the new width
|
|
1549
1307
|
// without contradicting anything, the thing DC-34's frontier rule
|
|
1550
1308
|
// existed to prevent.
|
|
1551
1309
|
this.#scrolledOff = 0;
|
|
@@ -1638,13 +1396,21 @@ export class Body {
|
|
|
1638
1396
|
// approval and the pick, ask-panel.ts for the ask), so nothing is
|
|
1639
1397
|
// lost anywhere.
|
|
1640
1398
|
if (panel !== null)
|
|
1641
|
-
return { status: panelStatusOf(panel), hint: undefined };
|
|
1399
|
+
return { status: panelStatusOf(panel), hint: undefined, expand: null };
|
|
1642
1400
|
// W22: while turns wait in the queue, the right hint shows the
|
|
1643
1401
|
// count — the chips below carry the lines themselves.
|
|
1644
1402
|
const queued = this.#queueState?.().length ?? 0;
|
|
1645
1403
|
if (queued > 0)
|
|
1646
|
-
return { status: this.#status, hint: `+${queued} queued
|
|
1647
|
-
|
|
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 };
|
|
1648
1414
|
}
|
|
1649
1415
|
/** Bind the editor's panel state — the PanelSelect slot occupant
|
|
1650
1416
|
* (W21: the panel replaces the live region + the input lead while
|
|
@@ -1775,49 +1541,17 @@ export class Body {
|
|
|
1775
1541
|
}
|
|
1776
1542
|
// ---- the one writer ----
|
|
1777
1543
|
/**
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
* the
|
|
1787
|
-
*
|
|
1788
|
-
* its count rides the line.
|
|
1789
|
-
*
|
|
1790
|
-
* What it fixes: a 28-call turn used to spend 28 rows of a 30-row
|
|
1791
|
-
* live region, so overflow was the NORM on real turns rather than
|
|
1792
|
-
* the edge — and a turn that overflows may not fold (R3f: a line
|
|
1793
|
-
* cannot claim rows already in the scrollback), which is why the
|
|
1794
|
-
* fold missed exactly the turns it exists for. The block's height
|
|
1795
|
-
* no longer depends on the call count at all.
|
|
1796
|
-
*
|
|
1797
|
-
* What it does NOT change: nothing about what commits or when. The
|
|
1798
|
-
* hold is untouched, the force-commit cap is untouched, and the
|
|
1799
|
-
* settle still produces the same fold it did before. That is the
|
|
1800
|
-
* 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.
|
|
1801
1554
|
*/
|
|
1802
|
-
/* DC-46 — `#liveRoom` RETIRED, and the reasoning with it.
|
|
1803
|
-
It capped the live region at `H − chrome − #committedLines`, to
|
|
1804
|
-
keep `skip` a function of `#committedLines` alone and therefore
|
|
1805
|
-
monotone. The argument was right about `skip` and wrong about the
|
|
1806
|
-
quantity: `#committedLines` is CUMULATIVE — re-derived over the
|
|
1807
|
-
whole line cache every frame, counting rows that left for the
|
|
1808
|
-
terminal's scrollback long ago — so one screenful into any session
|
|
1809
|
-
it exceeds H, the room clamps to its floor of one row, and every
|
|
1810
|
-
running call after that is a head row with its output gone.
|
|
1811
|
-
Measured at 24 and 40 rows with eight blank rows still on screen.
|
|
1812
|
-
|
|
1813
|
-
The live region is bounded by the SCREEN (the content cap the
|
|
1814
|
-
force-commit loop already uses). What holds the window's top is
|
|
1815
|
-
the `skip` clamp below: rows in [0, #scrolledOff) have reached the
|
|
1816
|
-
terminal's scrollback and are immutable, so the paint may not go
|
|
1817
|
-
back above them. A live region that GROWS scrolls committed rows
|
|
1818
|
-
away through #emitScroll, which is an append and not an un-scroll.
|
|
1819
|
-
The residue is a transient hole above the composer at a settle,
|
|
1820
|
-
bounded by the shrink itself and filled by the next commit. */
|
|
1821
1555
|
#liveProjection(W, ctx, cap) {
|
|
1822
1556
|
const rows = this.#project(W, ctx, CAP_PREVIEW);
|
|
1823
1557
|
if (cap === undefined || rows.length <= cap)
|
|
@@ -1858,39 +1592,19 @@ export class Body {
|
|
|
1858
1592
|
return [...heads.slice(0, keep), cutLine(`${p.dim} +${hidden} more${p.reset}`, W)];
|
|
1859
1593
|
}
|
|
1860
1594
|
/**
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
* nothing, so the live region's height was independent of the call
|
|
1868
|
-
* count. That was the answer to a 28-call turn spending 28 rows of a
|
|
1869
|
-
* 30-row region.
|
|
1870
|
-
*
|
|
1871
|
-
* The card answers it differently, and the ruling prefers this
|
|
1872
|
-
* answer: a running call is its own card at a FIXED height (E2), so
|
|
1873
|
-
* the height is a function of how many calls are IN FLIGHT rather
|
|
1874
|
-
* than of how many have happened — and the window shrinks to the room
|
|
1875
|
-
* before anything is force-committed (above). The live form and the
|
|
1876
|
-
* committed form are now the same form, which is what makes a settle
|
|
1877
|
-
* a change of content and never of position.
|
|
1878
|
-
*
|
|
1879
|
-
* What this keeps from R4: the height never moves ON ITS OWN. What it
|
|
1880
|
-
* gives up: the one-line summary of a stretch, which the owner ruled
|
|
1881
|
-
* 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).
|
|
1882
1601
|
*/
|
|
1883
1602
|
#project(W, ctx, budget) {
|
|
1884
1603
|
const out = [];
|
|
1885
|
-
const focus = this.#focusIndex();
|
|
1886
1604
|
const live = { ...ctx, liveWindow: budget };
|
|
1887
1605
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1888
1606
|
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1889
1607
|
const rows = cellComponent(this.#cells[i]).render(W, live);
|
|
1890
|
-
// the head row carries the affordance; the tint lands on it and
|
|
1891
|
-
// nowhere else, which is what makes "exactly one" structural
|
|
1892
|
-
if (i === focus && rows.length > 0)
|
|
1893
|
-
rows[0] = focusToken(rows[0], W);
|
|
1894
1608
|
out.push(...this.#space(i, prev, rows));
|
|
1895
1609
|
prev = rows;
|
|
1896
1610
|
}
|
|
@@ -1937,56 +1651,17 @@ export class Body {
|
|
|
1937
1651
|
* blanks are join artifacts — the count includes them (they are real
|
|
1938
1652
|
* screen rows), threaded against the previous sibling's OWN rows. */
|
|
1939
1653
|
liveCount() {
|
|
1940
|
-
const panel = this.#panelState?.() ?? null;
|
|
1941
|
-
const sheet = this.#sheetState?.() === true;
|
|
1942
|
-
const queueRows = this.#queueRows(this.#opts.width(), this.#opts.height());
|
|
1943
|
-
// KC1 §6: the composer's extra rows are chrome too — the scalar
|
|
1944
|
-
// counts them exactly like the menu/queue bands (N = 1 ⇒ +0)
|
|
1945
|
-
const inputExtra = this.#inputRows(this.#opts.width(), this.#opts.height(), this.#menuRows(this.#opts.width()).length, queueRows.length).rows.length - 1;
|
|
1946
|
-
// TUI2-R1 (D): the sheet occupies the live region, exactly like the
|
|
1947
|
-
// panel — the scalar must say so, or the cap arithmetic disagrees
|
|
1948
|
-
// with the screen.
|
|
1949
|
-
// R5 — the viewer occupies the live region exactly like the sheet,
|
|
1950
|
-
// so the scalar must say so, or the cap arithmetic disagrees with
|
|
1951
|
-
// the screen (the same rule DC-27 was about).
|
|
1952
|
-
if (this.#viewer !== null) {
|
|
1953
|
-
const capV = Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length);
|
|
1954
|
-
return this.#viewerBand(this.#opts.width()).slice(0, capV).length + CHROME_ROWS + inputExtra + queueRows.length;
|
|
1955
|
-
}
|
|
1956
|
-
if (sheet) {
|
|
1957
|
-
return (keysSheetRows(this.#opts.width()).slice(0, Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
1958
|
-
CHROME_ROWS +
|
|
1959
|
-
inputExtra +
|
|
1960
|
-
queueRows.length);
|
|
1961
|
-
}
|
|
1962
|
-
if (panel !== null) {
|
|
1963
|
-
// W21: the panel's own rows (the cap is exact — the scalar
|
|
1964
|
-
// reflects the screen). W22: the queue chips occupy their
|
|
1965
|
-
// own band — the panel's cap shrinks by their rows.
|
|
1966
|
-
return (panelRowsOf(panel, this.#opts.width(), Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
1967
|
-
CHROME_ROWS +
|
|
1968
|
-
inputExtra +
|
|
1969
|
-
queueRows.length);
|
|
1970
|
-
}
|
|
1971
|
-
// DC-27 — the scalar measures the PROJECTION, not a second render
|
|
1972
|
-
// of its own. This loop used to walk every live cell and render it
|
|
1973
|
-
// in full: no open-segment collapse, no flight rule, no act-slot
|
|
1974
|
-
// budget. After R3i that described a screen the compositor had
|
|
1975
|
-
// stopped drawing — for an open stretch with five finished calls
|
|
1976
|
-
// it counted five four-row blocks that were not there. Nothing
|
|
1977
|
-
// broke, because the force-commit loop measures liveLines.length
|
|
1978
|
-
// and the over-count is conservative; but the cap and geometry
|
|
1979
|
-
// gates were asserting a property of a function nothing paints
|
|
1980
|
-
// from, so a real regression in the region's height could not
|
|
1981
|
-
// have moved them. The rule this file already states for the
|
|
1982
|
-
// sheet ("the scalar must say so, or the cap arithmetic disagrees
|
|
1983
|
-
// with the screen") is the same rule here.
|
|
1984
|
-
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
1985
1654
|
const W = this.#opts.width();
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
const
|
|
1989
|
-
|
|
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;
|
|
1990
1665
|
}
|
|
1991
1666
|
/** The lines committed THIS frame — the writes land in the frame's
|
|
1992
1667
|
* committed section (the rows just above the live region). */
|
|
@@ -2008,242 +1683,156 @@ export class Body {
|
|
|
2008
1683
|
this.#paintedW = W;
|
|
2009
1684
|
this.#paintedH = H;
|
|
2010
1685
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
// the
|
|
2014
|
-
|
|
2015
|
-
//
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
//
|
|
2020
|
-
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
//
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
// The second is not a compromise of the first, it is the
|
|
2061
|
-
// answer to a question the first cannot reach. A cell can
|
|
2062
|
-
// STRADDLE the frontier — its head in the scrollback, its
|
|
2063
|
-
// tail still on screen — and the tail must be painted at the
|
|
2064
|
-
// current width. Holding its commit fold there emitted a
|
|
2065
|
-
// 100-column row into an 80-column screen and invariant ①
|
|
2066
|
-
// threw (60 → 100 → 80, measured). Fitting wins: a crash is
|
|
2067
|
-
// worse than a seam, and the seam a narrowing leaves is
|
|
2068
|
-
// rider 2's, stated rather than hidden.
|
|
2069
|
-
let row = 0;
|
|
2070
|
-
const refold = new Array(this.#committed).fill(false);
|
|
2071
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2072
|
-
const lines = this.#lineCache[i];
|
|
2073
|
-
if (lines === null || lines === undefined) {
|
|
2074
|
-
refold[i] = true;
|
|
2075
|
-
continue;
|
|
2076
|
-
}
|
|
2077
|
-
const above = row >= this.#scrolledOff;
|
|
2078
|
-
const fits = lines.every((l) => visibleWidth(l) <= W);
|
|
2079
|
-
refold[i] = above || !fits;
|
|
2080
|
-
const prev = i > 0 ? this.#lineCache[i - 1] : null;
|
|
2081
|
-
row += this.#space(i, prev ?? null, lines).length;
|
|
2082
|
-
}
|
|
2083
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2084
|
-
if (refold[i])
|
|
2085
|
-
this.#lineCache[i] = cellComponent(this.#cells[i]).render(W, ctx);
|
|
2086
|
-
}
|
|
2087
|
-
// #committedLines is re-derived over the WHOLE cache, because
|
|
2088
|
-
// the frozen prefix still occupies its own rows.
|
|
2089
|
-
this.#committedLines = 0;
|
|
2090
|
-
for (let i = 0; i < this.#committed; i += 1) {
|
|
2091
|
-
const lines = this.#lineCache[i] ?? cellComponent(this.#cells[i]).render(W, ctx);
|
|
2092
|
-
this.#lineCache[i] = lines;
|
|
2093
|
-
this.#committedLines += this.#space(i, i > 0 ? (this.#lineCache[i - 1] ?? []) : null, lines).length;
|
|
2094
|
-
}
|
|
2095
|
-
this.#refolded = refold.some(Boolean);
|
|
2096
|
-
}
|
|
2097
|
-
// 1. the natural commits — the leading DONE cells freeze: their
|
|
2098
|
-
// lines leave the live region, the scrolls + the committed
|
|
2099
|
-
// writes below place them (the #17 "freeze as a real line",
|
|
2100
|
-
// short sessions included — the frame coalescing keeps a
|
|
2101
|
-
// cell's first frame its freeze frame, so the frozen bytes
|
|
2102
|
-
// 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) {
|
|
2103
1735
|
this.#committedAtFrameStart = this.#committed;
|
|
2104
1736
|
this.#committedLinesThisFrame = [];
|
|
2105
|
-
|
|
2106
|
-
// cell of the OPEN quiet turn (no text yet) does not commit: its
|
|
2107
|
-
// committed form is decided at the release (the turn's text →
|
|
2108
|
-
// individual commits with the W13 rollups; the turn's end → the
|
|
2109
|
-
// fold). The FORCE-commit path below bypasses the hold — the
|
|
2110
|
-
// screen never sticks, the rollup degrades to individuals.
|
|
2111
|
-
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) {
|
|
2112
1738
|
this.#commitCell(this.#committed, W, ctx);
|
|
2113
1739
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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) {
|
|
2117
1749
|
const menuRows = this.#menuRows(W);
|
|
2118
|
-
// W22: the queue chips are the menu-rows family's other occupant
|
|
2119
|
-
// (the band above the box top) — the chrome rows and the live
|
|
2120
|
-
// caps account for both.
|
|
2121
1750
|
const queueRows = this.#queueRows(W, H);
|
|
2122
|
-
// KC1 §6: the input is N rows now (N = 1 ⇒ today's chrome exactly)
|
|
2123
|
-
// — chromeRows = 3 + N + menu + queue, and the content cap loses
|
|
2124
|
-
// the composer's EXTRA rows the same way it loses the bands.
|
|
2125
1751
|
const editor = this.#inputRows(W, H, menuRows.length, queueRows.length);
|
|
2126
1752
|
const inputExtra = editor.rows.length - 1;
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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() {
|
|
2143
1777
|
const sheetUp = this.#sheetState?.() === true;
|
|
2144
|
-
// R5 — the viewer is an overlay of exactly the same kind, so it
|
|
2145
|
-
// joins the same flag. That one word is what buys it the whole
|
|
2146
|
-
// zero-litter discipline below: the window freezes, #emitScroll
|
|
2147
|
-
// is skipped, and the close repaints from #lastSkip.
|
|
2148
1778
|
const viewerUp = this.#viewer !== null;
|
|
2149
1779
|
this.#overlayFrame = sheetUp || this.#sheetWasUp || viewerUp || this.#viewerWasUp;
|
|
2150
1780
|
this.#sheetWasUp = sheetUp;
|
|
2151
1781
|
this.#viewerWasUp = viewerUp;
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2175
|
-
const frame = panelFrameOf(panel, W,
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
// This loop committed `#committed` unconditionally. R4's standing
|
|
2195
|
-
// slot held the live region at a constant height, so the
|
|
2196
|
-
// projection never grew past the cap on its own and the loop never
|
|
2197
|
-
// reached a running cell; R13 retired the slot (DC-46) and it
|
|
2198
|
-
// promptly did. Three parallel searches, the last two settling
|
|
2199
|
-
// first: their cards pushed the region over the cap, the loop
|
|
2200
|
-
// committed the FIRST call — still in flight — froze its
|
|
2201
|
-
// three-row running card and its breathing mark into the
|
|
2202
|
-
// scrollback, and stepped `#committed` past it. When the result
|
|
2203
|
-
// arrived there was no live cell left to draw it into, so the work
|
|
2204
|
-
// never reached the screen at all.
|
|
2205
|
-
//
|
|
2206
|
-
// A cell that is not done has no committed form yet; committing
|
|
2207
|
-
// one is writing history that has not happened. When the head is
|
|
2208
|
-
// running, the region gives way instead — the cards behind it
|
|
2209
|
-
// degrade first, then the running window shrinks (DC-43's own
|
|
2210
|
-
// ladder, in #liveProjection), and the remainder is counted.
|
|
2211
|
-
// The guard is on a RUNNING TOOL CALL specifically, not on any
|
|
2212
|
-
// unfinished cell. A streaming text or raw cell HAS a committed
|
|
2213
|
-
// form — the rows it has already written are final, append-only —
|
|
2214
|
-
// and spilling those is exactly what this loop is for. A running
|
|
2215
|
-
// call does not: its card changes shape at the settle, so a
|
|
2216
|
-
// committed one is a row that will never be corrected.
|
|
2217
|
-
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)
|
|
2218
1823
|
this.#committed < this.#cells.length &&
|
|
2219
1824
|
!(this.#cells[this.#committed].kind === "tool" && !this.#cells[this.#committed].done)) {
|
|
2220
|
-
// R3f: the cell about to be force-committed marks its segment
|
|
2221
|
-
// SPILLED. The rule was written at R3b — "a segment too big for
|
|
2222
|
-
// the screen already has rows in the scrollback that cannot be
|
|
2223
|
-
// taken back, so it renders normally and does not collapse" —
|
|
2224
|
-
// and then never wired: `spilled` had a declaration, an
|
|
2225
|
-
// initializer and a read, and nothing ever set it. The read was
|
|
2226
|
-
// therefore vacuously true, so a 43-call turn force-committed
|
|
2227
|
-
// thirty expanded rows and STILL printed `✦ thought 103s · 43
|
|
2228
|
-
// reads` underneath them, claiming as folded the work standing
|
|
2229
|
-
// visible above it.
|
|
2230
|
-
this.#markSpilled(this.#committed);
|
|
2231
1825
|
this.#commitCell(this.#committed, W, ctx);
|
|
2232
1826
|
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
2233
1827
|
// pointed at may have just left the live region.
|
|
2234
|
-
|
|
1828
|
+
lines = this.#liveProjection(W, ctx, cap);
|
|
2235
1829
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
// frame. A click is answered against the frame the human was looking
|
|
2243
|
-
// at when they clicked, which is this one.
|
|
2244
|
-
this.#panelRowSpan =
|
|
2245
|
-
panelSpan === null ? null : { top: liveTop + panelSpan.offset, count: panelSpan.count, first: panelSpan.first };
|
|
2246
|
-
// 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) {
|
|
2247
1836
|
const out = [];
|
|
2248
1837
|
// REL-0152-D14 — AUTOWRAP OFF for the frame's duration.
|
|
2249
1838
|
//
|
|
@@ -2324,28 +1913,12 @@ export class Body {
|
|
|
2324
1913
|
// never paired with a ?25h. Sync (2026) still brackets the frame
|
|
2325
1914
|
// where the terminal understands it.
|
|
2326
1915
|
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h\x1b[?25l");
|
|
2327
|
-
//
|
|
2328
|
-
//
|
|
2329
|
-
//
|
|
2330
|
-
//
|
|
2331
|
-
//
|
|
2332
|
-
|
|
2333
|
-
// stale pass erases the old live rows, and nothing re-paints the
|
|
2334
|
-
// band between the committed window and the new liveTop (finding
|
|
2335
|
-
// #A8 — the W11-boundary pileup). A GROWTH is safe on the steady
|
|
2336
|
-
// path (the new live's bottom-anchored extent covers the old — the
|
|
2337
|
-
// erased rows are all re-painted); a shrink takes the full-redraw
|
|
2338
|
-
// path: the window re-paints at the model's positions, every row
|
|
2339
|
-
// covered (the V6-1 every-row rule).
|
|
2340
|
-
// REL-0152-R1: ONE renderer. The steady/full split existed because
|
|
2341
|
-
// the steady path moved rows by scrolling and could not handle a
|
|
2342
|
-
// window that moved the wrong way, so the frames it could not draw
|
|
2343
|
-
// were handed to a full repaint. A diff has no such frames: it
|
|
2344
|
-
// emits the rows that differ, and on a frame where everything
|
|
2345
|
-
// differs that IS a full repaint. The dispatch, the two geometries
|
|
2346
|
-
// and the invariant about which moves each path may use all go
|
|
2347
|
-
// with it.
|
|
2348
|
-
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);
|
|
2349
1922
|
this.#fullRedraw = false;
|
|
2350
1923
|
// REL-0161: the frame close no longer shows the cursor — hidden IS
|
|
2351
1924
|
// the steady state (Terminal.app infers "a prompt line" from
|
|
@@ -2363,19 +1936,11 @@ export class Body {
|
|
|
2363
1936
|
}
|
|
2364
1937
|
this.#lastLiveTop = liveTop;
|
|
2365
1938
|
this.#lastLiveRows = liveRowsTotal;
|
|
2366
|
-
this.#lastInputRows = editor.rows.length;
|
|
1939
|
+
this.#lastInputRows = chrome.editor.rows.length;
|
|
2367
1940
|
// KC1 §6: the next steady frame's relative moves start where THIS
|
|
2368
1941
|
// frame's CHA parked the cursor — the marker's row inside the
|
|
2369
1942
|
// composer (N = 1 ⇒ H−2, the retired hard-coded anchor).
|
|
2370
|
-
this.#lastAnchorRow = H - 1 - editor.rows.length + editor.markerRow;
|
|
2371
|
-
}
|
|
2372
|
-
/** R13 D1 — the same constant as everything else. This used to compute
|
|
2373
|
-
* the lead from a ONE-ROW STAND-IN so the live block would claim the
|
|
2374
|
-
* spacing its fold was going to get; with the rhythm constant there
|
|
2375
|
-
* is nothing to simulate, and R7a's device retires with the formula
|
|
2376
|
-
* that needed it. */
|
|
2377
|
-
#blockSpace(i, prev, rows) {
|
|
2378
|
-
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
1943
|
+
this.#lastAnchorRow = H - 1 - chrome.editor.rows.length + chrome.editor.markerRow;
|
|
2379
1944
|
}
|
|
2380
1945
|
#space(i, prev, rows) {
|
|
2381
1946
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
@@ -2384,18 +1949,9 @@ export class Body {
|
|
|
2384
1949
|
// so a settle changes content and never position by construction.
|
|
2385
1950
|
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
2386
1951
|
}
|
|
2387
|
-
/**
|
|
2388
|
-
*
|
|
2389
|
-
*
|
|
2390
|
-
* The spacing formula reads what stood above; a cell that rendered
|
|
2391
|
-
* nothing did not stand above anything. Since R3d whole families of
|
|
2392
|
-
* cells render `[]` — the members a fold speaks for — and the
|
|
2393
|
-
* formula was reading that empty array as "a zero-row sibling", so a
|
|
2394
|
-
* multi-row block following a fold lost the blank that belongs above
|
|
2395
|
-
* it. The defect predates this round (any folded turn followed by a
|
|
2396
|
-
* raw block has it); R3i's projection is what finally put a test on
|
|
2397
|
-
* the path.
|
|
2398
|
-
*/
|
|
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). */
|
|
2399
1955
|
#lastDrawn(i, prev) {
|
|
2400
1956
|
if (prev !== null && prev.length > 0)
|
|
2401
1957
|
return prev;
|
|
@@ -2414,236 +1970,36 @@ export class Body {
|
|
|
2414
1970
|
* this cell's commit — the cache stays raw, the placed rows count. */
|
|
2415
1971
|
#commitCell(i, W, ctx) {
|
|
2416
1972
|
const cell = this.#cells[i];
|
|
2417
|
-
const lines =
|
|
2418
|
-
// W15: a tool cell whose committed rows
|
|
2419
|
-
//
|
|
2420
|
-
//
|
|
2421
|
-
//
|
|
2422
|
-
//
|
|
2423
|
-
|
|
2424
|
-
// key does not answer would be the one thing worse than silence.
|
|
2425
|
-
// unshift: the cells commit oldest-first, so the NEWEST cut lands
|
|
2426
|
-
// at the front — the expand pointer's "newest back" walk starts
|
|
2427
|
-
// where the user's last key press would aim.
|
|
2428
|
-
// R3b: a fold HEAD joins the ring too. The test used to demand a
|
|
2429
|
-
// tool cell, and a segment's fold can be emitted at a thinking
|
|
2430
|
-
// cell — which would have left the whole segment unreachable by
|
|
2431
|
-
// the very key its own row advertises.
|
|
2432
|
-
// R4a — the ring captures by IDENTITY, not by searching our own
|
|
2433
|
-
// printed bytes.
|
|
2434
|
-
//
|
|
2435
|
-
// This used to require the rendered rows to contain the literal
|
|
2436
|
-
// "ctrl+o", which made the affordance LOAD-BEARING: retiring the
|
|
2437
|
-
// printed key (the owner's ruling) would have silently emptied the
|
|
2438
|
-
// ring and taken the expand key with it — not a missing hint, a
|
|
2439
|
-
// missing feature. A fold head is a fold head because the segment
|
|
2440
|
-
// says so; a tool cell is expandable when it is hiding rows.
|
|
2441
|
-
const isFoldHead = this.#segmentOf(i)?.headCell === i;
|
|
2442
|
-
const hidesRows = cell.kind === "tool" && lines.some((l) => l.includes("ctrl+o"));
|
|
2443
|
-
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")))
|
|
2444
1980
|
this.#collapsed.unshift(i);
|
|
2445
|
-
// R4a's walk reset retired with the walk (DC-50): there is no
|
|
2446
|
-
// "which one does it open" any more — one press opens all of
|
|
2447
|
-
// them.
|
|
2448
|
-
}
|
|
2449
1981
|
this.#lineCache[i] = lines;
|
|
2450
1982
|
const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
2451
1983
|
this.#committed += 1;
|
|
2452
1984
|
this.#committedLines += placed.length;
|
|
2453
1985
|
this.#committedLinesThisFrame.push(...placed);
|
|
2454
1986
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
* needs a human's eye. Law 1.3 makes the same call about marks — a
|
|
2472
|
-
* failure keeps its colour AND its words — and this is that rule at
|
|
2473
|
-
* the scale of a run.
|
|
2474
|
-
*
|
|
2475
|
-
* The cost, accepted: a turn that reads twenty files and hits one
|
|
2476
|
-
* denial keeps all twenty rows. The alternative is a screen that says
|
|
2477
|
-
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
2478
|
-
*/
|
|
2479
|
-
/**
|
|
2480
|
-
* R3i — the trouble the stretch met, as the line's own terms.
|
|
2481
|
-
*
|
|
2482
|
-
* Law 1.3: an outcome is stated in WORDS, "the only form that
|
|
2483
|
-
* survives a pipe". So the kind is a different word, never a
|
|
2484
|
-
* different colour — `2 failed`, `1 denied`, `1 interrupted` — and
|
|
2485
|
-
* the failure's identity rides with it. In this phase the terms are
|
|
2486
|
-
* only DRAWN (the live line names trouble the moment it happens);
|
|
2487
|
-
* whether trouble still blocks the fold is the next phase's ruling.
|
|
2488
|
-
*/
|
|
2489
|
-
/** R3i — is this cell one the fold must not count as work done? */
|
|
2490
|
-
#cellInTrouble(i) {
|
|
2491
|
-
const c = this.#cells[i];
|
|
2492
|
-
if (c === undefined || c.kind !== "tool")
|
|
2493
|
-
return false;
|
|
2494
|
-
return c.isError || c.reason !== null || c.verdict?.decision === "denied";
|
|
2495
|
-
}
|
|
2496
|
-
#segmentTroubleTerms(seg) {
|
|
2497
|
-
let failed = 0;
|
|
2498
|
-
let denied = 0;
|
|
2499
|
-
let interrupted = 0;
|
|
2500
|
-
let what = "";
|
|
2501
|
-
for (const j of seg.cells) {
|
|
2502
|
-
const c = this.#cells[j];
|
|
2503
|
-
if (c === undefined || c.kind !== "tool")
|
|
2504
|
-
continue;
|
|
2505
|
-
// WHICH call, and WHY. The target alone answers the first and
|
|
2506
|
-
// not the second, and for a policy denial the second is the
|
|
2507
|
-
// whole point: `sub/out.txt` does not tell a human that plan
|
|
2508
|
-
// mode is read-only, and that sentence is the one they act
|
|
2509
|
-
// on. Law 1.3's own words — an outcome is stated in words —
|
|
2510
|
-
// and the ladder cuts this clause last, so it degrades to the
|
|
2511
|
-
// target before it disappears.
|
|
2512
|
-
const named = () => {
|
|
2513
|
-
const t = toolTarget(c.name, JSON.parse(c.inputFull));
|
|
2514
|
-
const why = c.verdict?.reason ?? c.reason;
|
|
2515
|
-
return why === null || why === undefined || why === "" || why === "interrupted" ? t : `${t} (${why})`;
|
|
2516
|
-
};
|
|
2517
|
-
if (c.verdict?.decision === "denied" || (c.reason !== null && c.reason !== "interrupted" && /denied/i.test(c.reason))) {
|
|
2518
|
-
denied += 1;
|
|
2519
|
-
if (what === "")
|
|
2520
|
-
what = named();
|
|
2521
|
-
}
|
|
2522
|
-
else if (c.reason === "interrupted") {
|
|
2523
|
-
interrupted += 1;
|
|
2524
|
-
}
|
|
2525
|
-
else if (c.isError || c.reason !== null) {
|
|
2526
|
-
failed += 1;
|
|
2527
|
-
if (what === "")
|
|
2528
|
-
what = named();
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
const out = [];
|
|
2532
|
-
if (failed > 0)
|
|
2533
|
-
out.push(["failed", failed, what]);
|
|
2534
|
-
if (denied > 0)
|
|
2535
|
-
out.push(["denied", denied, what]);
|
|
2536
|
-
if (interrupted > 0)
|
|
2537
|
-
out.push(["interrupted", interrupted, ""]);
|
|
2538
|
-
return out;
|
|
2539
|
-
}
|
|
2540
|
-
#segmentHasTrouble(seg) {
|
|
2541
|
-
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
2542
|
-
// exists for, and it was the one case the predicate could not
|
|
2543
|
-
// see — a denial carrying no `reason` string leaves isError
|
|
2544
|
-
// false and reason null, so `✦ thought 3s · 20 reads` could
|
|
2545
|
-
// stand over a refused write. The verdict is the record of it.
|
|
2546
|
-
return this.#segmentTools(seg).some((c) => c.isError || c.reason !== null || c.verdict?.decision === "denied");
|
|
2547
|
-
}
|
|
2548
|
-
/** R3b — the segment's TOOL cells, in order. */
|
|
2549
|
-
#segmentTools(seg) {
|
|
2550
|
-
const out = [];
|
|
2551
|
-
for (const j of seg.cells) {
|
|
2552
|
-
const c = this.#cells[j];
|
|
2553
|
-
if (c.kind === "tool")
|
|
2554
|
-
out.push(c);
|
|
2555
|
-
}
|
|
2556
|
-
return out;
|
|
2557
|
-
}
|
|
2558
|
-
/** R3f — the cell is leaving the live region under the screen's hard
|
|
2559
|
-
* cap, so its segment can no longer be represented by a fold. */
|
|
2560
|
-
#markSpilled(i) {
|
|
2561
|
-
const seg = this.#segmentOf(i);
|
|
2562
|
-
if (seg !== null)
|
|
2563
|
-
seg.spilled = true;
|
|
2564
|
-
}
|
|
2565
|
-
/** R3b — the segment a committed cell belongs to, or null when it has
|
|
2566
|
-
* none (a cell of the pipe path, or a kind that is not work). */
|
|
2567
|
-
#segmentOf(i) {
|
|
2568
|
-
const cell = this.#cells[i];
|
|
2569
|
-
if (cell.kind !== "thinking" && cell.kind !== "tool")
|
|
2570
|
-
return null;
|
|
2571
|
-
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2572
|
-
const si = this.#cellSegment[i];
|
|
2573
|
-
if (turn === undefined || si === undefined || si < 0)
|
|
2574
|
-
return null;
|
|
2575
|
-
return turn.segments[si] ?? null;
|
|
2576
|
-
}
|
|
2577
|
-
/** W14 — the fold-hold: a thinking/tool cell of the OPEN quiet turn
|
|
2578
|
-
* (no text yet) does not commit — its committed form is decided at
|
|
2579
|
-
* the release. The cell's OWN turn must be the CURRENT one (a cell
|
|
2580
|
-
* of a released turn commits normally). The force-commit path never
|
|
2581
|
-
* consults this — the screen's hard cap wins over the hold. */
|
|
2582
|
-
#held(i) {
|
|
2583
|
-
// R13 — NOTHING IS HELD ANY MORE. DECLARED REVERSAL of W14's
|
|
2584
|
-
// quiet-turn hold, R3b/R3i's segment hold and TUI2-R1.5 ①'s
|
|
2585
|
-
// explore-run hold, all three of them owner-ruled away on
|
|
2586
|
-
// 2026-09-03 with the mechanism they served.
|
|
2587
|
-
//
|
|
2588
|
-
// Every one of them existed for the same reason: a done cell's
|
|
2589
|
-
// COMMITTED FORM was undecided while its segment or its run was
|
|
2590
|
-
// still open, because a fold line or a rollup row might yet stand
|
|
2591
|
-
// for it — and a committed row cannot be taken back (ADR-0046).
|
|
2592
|
-
// With no fold and no rollup, a call's committed form is its own
|
|
2593
|
-
// card and is settled the instant the call is: there is nothing
|
|
2594
|
-
// left to wait for.
|
|
2595
|
-
//
|
|
2596
|
-
// It is not only dead weight. Holding done cells in the live
|
|
2597
|
-
// region made the region carry work that was FINISHED, so a burst
|
|
2598
|
-
// of four reads and a shell in flight competed for the same rows
|
|
2599
|
-
// and DC-43's shrink took the running call's output away — the
|
|
2600
|
-
// one thing on the screen the human is waiting for (R7a D). Let
|
|
2601
|
-
// them commit and the room is there.
|
|
2602
|
-
void i;
|
|
2603
|
-
return false;
|
|
2604
|
-
}
|
|
2605
|
-
/* R13 — `#growingRun` retired with the rollup's commit hold (#held). */
|
|
2606
|
-
/** W14/W13 — the release-time decision at a commit, BEFORE the cell's
|
|
2607
|
-
* own render: the folded-turn fold first (a QUIET turn — ended, no
|
|
2608
|
-
* text — becomes the ONE fold line; the rest of its thinking/tool
|
|
2609
|
-
* cells render [] after the fold), then the W13 rollup (a text
|
|
2610
|
-
* turn's N > 2 same-tool run: the HEAD renders the group summary,
|
|
2611
|
-
* the members render [] — the scan is the work order's "group key",
|
|
2612
|
-
* derived at commit time, never pre-stored). */
|
|
2613
|
-
#foldOrRollup(cell, i, W, ctx) {
|
|
2614
|
-
// R13 — DECLARED REVERSAL, three at once, all of them the same
|
|
2615
|
-
// idea and all of them owner-ruled on 2026-09-03:
|
|
2616
|
-
//
|
|
2617
|
-
// · the SEGMENT FOLD (R3b–R3i, W14) — a closed stretch of two or
|
|
2618
|
-
// more cells collapsed into one settled line;
|
|
2619
|
-
// · the W13 ROLLUP and the `rolled` cell field — a run of three
|
|
2620
|
-
// or more same-tool calls collapsed into a group summary;
|
|
2621
|
-
// · TUI2-R1 (B)'s EXPLORATION ROW — the mixed read-only variant
|
|
2622
|
-
// of the same collapse.
|
|
2623
|
-
//
|
|
2624
|
-
// Every one of them answered the same pressure: ungrounded output
|
|
2625
|
-
// rows owned the screen, so work was collapsed into sentences
|
|
2626
|
-
// ABOUT the work. The card is what changes that arithmetic — a
|
|
2627
|
-
// call's rows sit inside a surface that says where it begins and
|
|
2628
|
-
// ends, so five of them read as one object rather than five loose
|
|
2629
|
-
// lines — and the owner's ruling is that the collapse costs more
|
|
2630
|
-
// than it buys: a page where the machine's work is sometimes a
|
|
2631
|
-
// card, sometimes a summary line and sometimes nothing at all is
|
|
2632
|
-
// a page a reader cannot predict. One rhythm, one surface.
|
|
2633
|
-
//
|
|
2634
|
-
// So every cell renders itself, and this method is the record of
|
|
2635
|
-
// what used to happen here. What the folds bought is bought
|
|
2636
|
-
// differently now: the preview cap (five rows a call, E1's read
|
|
2637
|
-
// showing none) is what keeps a burst from owning the screen, and
|
|
2638
|
-
// it is a CONSTANT per call rather than a decision about runs.
|
|
2639
|
-
//
|
|
2640
|
-
// What goes with them: `#growingRun`'s commit hold existed only so
|
|
2641
|
-
// a run's committed FORM could be decided once the run closed —
|
|
2642
|
-
// with no run-level form left to decide, a done cell commits when
|
|
2643
|
-
// it is done. `#rolledHeads`, `rolledOf`, `rolledTitle`,
|
|
2644
|
-
// `rolledDetail` and the `rolled` field go with the rollup.
|
|
2645
|
-
return cellComponent(cell).render(W, ctx);
|
|
2646
|
-
}
|
|
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. */
|
|
2647
2003
|
/** The slot occupant's extra rows — the slash-command menu (above the
|
|
2648
2004
|
* status, in the rhythm gap + the content's spare rows — the old
|
|
2649
2005
|
* menu's position, slot-shaped). */
|
|
@@ -3020,7 +2376,6 @@ export class Body {
|
|
|
3020
2376
|
// history it had just erased, and everything above was simply
|
|
3021
2377
|
// GONE: `dc34-widen-seam` measured 36 tokens missing from the
|
|
3022
2378
|
// scrollback the first time this branch was left in place.
|
|
3023
|
-
this.#refolded = false;
|
|
3024
2379
|
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
3025
2380
|
this.#resizeFrame = false;
|
|
3026
2381
|
}
|
|
@@ -3058,26 +2413,12 @@ export class Body {
|
|
|
3058
2413
|
// OLDEST on screen — they are still in the model and come back on
|
|
3059
2414
|
// the close.
|
|
3060
2415
|
const contentRows = Math.max(0, H - CHROME_ROWS - inputExtra - queueRows.length - menuRows.length);
|
|
3061
|
-
//
|
|
3062
|
-
//
|
|
3063
|
-
//
|
|
3064
|
-
//
|
|
3065
|
-
//
|
|
3066
|
-
|
|
3067
|
-
// cells above the frontier, or the live band collapsing — and
|
|
3068
|
-
// nothing stopped it (rider 3's ungated reach-back).
|
|
3069
|
-
//
|
|
3070
|
-
// Clamping costs a gap under short content for one frame, which
|
|
3071
|
-
// the next commit fills. Reaching back costs a duplicate that
|
|
3072
|
-
// stands in the transcript forever.
|
|
3073
|
-
// ...but only when the WIDTH moved. A height change re-indexes
|
|
3074
|
-
// nothing — the folds are untouched, every row means what it
|
|
3075
|
-
// meant — so reaching back there is the pre-existing behaviour a
|
|
3076
|
-
// gate already covers (the A8 windowing case: grow the screen and
|
|
3077
|
-
// the banner returns). The duplication measured in this round is
|
|
3078
|
-
// width-driven, and so is the guard.
|
|
3079
|
-
const march = all.slice(this.#lastW !== 0 && this.#lastW !== W ? Math.max(skip, this.#scrolledOff) : skip);
|
|
3080
|
-
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);
|
|
3081
2422
|
for (const line of march.length > contentRows ? march.slice(march.length - contentRows) : march) {
|
|
3082
2423
|
desired[r - 1] = this.#checked(line, W);
|
|
3083
2424
|
r += 1;
|
|
@@ -3101,7 +2442,7 @@ export class Body {
|
|
|
3101
2442
|
desired[H - 2 - inputExtra + i - 1] = this.#checked(editor.rows[i], W);
|
|
3102
2443
|
desired[H - 1 - 1] = boxBottom(W);
|
|
3103
2444
|
const statusRow = this.#statusSource();
|
|
3104
|
-
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);
|
|
3105
2446
|
this.#emitDiff(out, W, H, desired);
|
|
3106
2447
|
// REL-0152-R1: park from where the cursor ACTUALLY is — see
|
|
3107
2448
|
// #cursorRow. It used to be parked from H, which the bottom-up
|