@vincemakes/kiso-tui-cells 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/components.d.ts +27 -204
- package/dist/components.js +161 -582
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.d.ts +13 -17
- package/dist/render.js +54 -19
- package/dist/strings.js +32 -34
- package/dist/width.d.ts +8 -0
- package/dist/width.js +20 -0
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
* (untouched); render.ts supplies the original text (palette, escape,
|
|
26
26
|
* tint, fold wording).
|
|
27
27
|
*/
|
|
28
|
-
import { displayWidth, visibleWidth } from "./width.js";
|
|
28
|
+
import { displayWidth, visibleWidth, widthCut } from "./width.js";
|
|
29
29
|
// TUI2-R2pre ④: the ONE display-verb table (strings.ts, beside
|
|
30
30
|
// KEY_BINDINGS). strings.js imports only render/width here, so this edge
|
|
31
31
|
// adds no cycle.
|
|
32
32
|
import { displayVerb } from "./strings.js";
|
|
33
|
-
import { bannerLines, breathFrame, escapeTerminal, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
|
|
33
|
+
import { bannerLines, breathFrame, cutLine, escapeTerminal, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
|
|
34
34
|
// TUI2-MD: the markdown renderer's surface reaches the tui through this
|
|
35
35
|
// module (the tui's components shim re-exports it) — one import edge,
|
|
36
36
|
// and it points one way: md.ts measures with the width authority, never
|
|
@@ -113,7 +113,8 @@ export function foldLine(line, W) {
|
|
|
113
113
|
* renderer can measure without importing this module back — the
|
|
114
114
|
* re-export is verbatim, so every existing importer and the barrel see
|
|
115
115
|
* exactly what they saw. */
|
|
116
|
-
export { visibleWidth } from "./width.js";
|
|
116
|
+
export { visibleWidth, widthCut } from "./width.js";
|
|
117
|
+
export { cutLine } from "./render.js";
|
|
117
118
|
/** The W11 spacing formula — "a row gets one blank line above it when
|
|
118
119
|
* the row is itself a block, or when the previous sibling was taller
|
|
119
120
|
* than one row". One-row siblings pack tight; anything multi-row
|
|
@@ -122,7 +123,7 @@ export { visibleWidth } from "./width.js";
|
|
|
122
123
|
* `prev` is the previous sibling's OWN rows (raw — a cell's own blank
|
|
123
124
|
* must never count toward its height). The blank is a JOIN artifact:
|
|
124
125
|
* the cell's own render stays blank-free, so per-cell accounting
|
|
125
|
-
* (heights, the
|
|
126
|
+
* (heights, the line cache) never sees a fake row. */
|
|
126
127
|
export function bodySpacing(prev, rows) {
|
|
127
128
|
if (rows.length === 0 || prev === null || prev.length === 0)
|
|
128
129
|
return rows;
|
|
@@ -168,9 +169,9 @@ export function cellComponent(cell) {
|
|
|
168
169
|
case "user":
|
|
169
170
|
return new UserMessage(cell);
|
|
170
171
|
case "thinking":
|
|
171
|
-
// R7: the committed/live surface is the BLOCK
|
|
172
|
-
//
|
|
173
|
-
//
|
|
172
|
+
// R7: the committed/live surface is the BLOCK; the pipe path
|
|
173
|
+
// prints render.ts's one-row foldThinking instead, whose bytes
|
|
174
|
+
// are asserted by the --plain identity gate.
|
|
174
175
|
return new ThinkingBlock(cell);
|
|
175
176
|
case "tool":
|
|
176
177
|
return new ToolExecution(cell);
|
|
@@ -334,19 +335,14 @@ export function pendingQueueRows(lines, W) {
|
|
|
334
335
|
}
|
|
335
336
|
return out;
|
|
336
337
|
}
|
|
337
|
-
/** The thinking fold — one dim line, width-capped so the /think suffix
|
|
338
|
-
* rides the fold's own row (the #17 fix's slice, componentized). The
|
|
339
|
-
* slice is DISPLAY-WIDTH-based (the char-based slice overflowed with
|
|
340
|
-
* CJK — 2 cells per char — and tripped invariant ① on a real
|
|
341
|
-
* Chinese session). W2: the leading ⋯ is the thinking gutter — the
|
|
342
|
-
* midline mark (the state), never the text ellipsis (the truncation). */
|
|
343
338
|
/**
|
|
344
339
|
* R7 — THINKING IS A BLOCK OF WORDS.
|
|
345
340
|
*
|
|
346
341
|
* The owner's ruling, arrived at from a side-by-side with pi's screen:
|
|
347
342
|
* the model's reasoning reads as its own paragraphs — italic, dim,
|
|
348
|
-
* indented two — and is never folded away.
|
|
349
|
-
*
|
|
343
|
+
* indented two — and is never folded away. A one-row fold (the pipe
|
|
344
|
+
* path's `foldThinking` is what remains of it) stood for it, with a
|
|
345
|
+
* key; four rounds
|
|
350
346
|
* of machinery were then built to hand the rest back, and the owner's
|
|
351
347
|
* complaint through all of them was the same sentence: I cannot see
|
|
352
348
|
* what it was thinking.
|
|
@@ -411,79 +407,18 @@ class ThinkingBlock {
|
|
|
411
407
|
return rows;
|
|
412
408
|
}
|
|
413
409
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
// `escapeTerminal` strips C0 but KEEPS \n and \t, and
|
|
425
|
-
// `charWidth(0x0A)` is 1 — so a multi-line thinking block sailed
|
|
426
|
-
// through every width check as a legal single row, and the
|
|
427
|
-
// terminal then wrapped it across the chrome. That shipped in
|
|
428
|
-
// 0.16.6 and is what smashed the composer: a model whose thinking
|
|
429
|
-
// opens with a numbered plan ("1. …\n2. …") produces exactly it.
|
|
430
|
-
// Invariant ①b now catches the class at the emit; this stops
|
|
431
|
-
// producing it. Whitespace collapses because the row is a
|
|
432
|
-
// SUMMARY — the full text is one ctrl+o away, unchanged.
|
|
433
|
-
const trimmed = escapeTerminal(block.trim()).replace(/\s+/g, " ");
|
|
434
|
-
// R2 (owner, 2026-08-27) — three changes, each independent.
|
|
435
|
-
//
|
|
436
|
-
// ITALIC marks the row as not-the-answer without spending a colour,
|
|
437
|
-
// on the same argument that admitted italic to the alphabet.
|
|
438
|
-
//
|
|
439
|
-
// The cut lands on a WORD. It used to cut on a byte, so the fold
|
|
440
|
-
// read as `…the user's h` and the reader's eye had to reassemble a
|
|
441
|
-
// word it already knew.
|
|
442
|
-
//
|
|
443
|
-
// The affordance moves to the RIGHT EDGE, so the left edge of every
|
|
444
|
-
// row on screen is content. The char count goes with the move: it
|
|
445
|
-
// told the reader nothing they could act on, and the row it was
|
|
446
|
-
// crowding is the one thing this cell says.
|
|
447
|
-
//
|
|
448
|
-
// The ≤100 short-circuit stays width-aware: a short block at a
|
|
449
|
-
// narrow width returned the line UNFOLDED and tripped invariant ①.
|
|
450
|
-
// DC-15: the affordance is DROPPED, not squeezed. `room` floored at
|
|
451
|
-
// 1 while `pad` floored at 1 independently, so a narrow terminal
|
|
452
|
-
// produced 2 + cut + pad + 6 = 11 cells no matter what W was — and
|
|
453
|
-
// invariant ① does not truncate, it THROWS. Measured 11 cells at
|
|
454
|
-
// every W ≤ 10. Below the width where the tail and one word of
|
|
455
|
-
// content can both stand, the row is the CONTENT: a cell that
|
|
456
|
-
// cannot hold the key's name has nothing to say about the key.
|
|
457
|
-
const tail = "/think";
|
|
458
|
-
const room = W - 2 - tail.length - 1;
|
|
459
|
-
// the belt: cutLine is SGR-aware and single-row, so the invariant
|
|
460
|
-
// holds by CONSTRUCTION at every width rather than by arithmetic
|
|
461
|
-
// that has to be re-proved every time a span moves.
|
|
462
|
-
if (room < 2)
|
|
463
|
-
return [cutLine(`${p.dim}⋯ ${p.italic}${wordCut(trimmed, Math.max(1, W - 2))}${p.italicEnd}${p.reset}`, W)];
|
|
464
|
-
const cut = wordCut(trimmed, room);
|
|
465
|
-
const body = `⋯ ${p.italic}${cut}${p.italicEnd}`;
|
|
466
|
-
const pad = Math.max(1, W - 2 - visibleWidth(cut) - tail.length);
|
|
467
|
-
return [cutLine(`${p.dim}${body}${" ".repeat(pad)}${tail}${p.reset}`, W)];
|
|
410
|
+
/** The SGR spans still open at the end of `text`, given those open at
|
|
411
|
+
* its start. A reset closes everything; anything else stacks. */
|
|
412
|
+
function spansOpenAfter(text, before) {
|
|
413
|
+
let open = [...before];
|
|
414
|
+
for (const m of text.matchAll(/\x1b\[[0-9;]*m/g)) {
|
|
415
|
+
if (m[0] === "\x1b[0m")
|
|
416
|
+
open = [];
|
|
417
|
+
else
|
|
418
|
+
open.push(m[0]);
|
|
468
419
|
}
|
|
420
|
+
return open;
|
|
469
421
|
}
|
|
470
|
-
/** R2 — cut at a word boundary, with the honest ellipsis. widthCut cuts
|
|
471
|
-
* at a cell, which is right for verbatim output and wrong for prose:
|
|
472
|
-
* the reader has to reassemble "h" into "home". Falls back to the cell
|
|
473
|
-
* cut when the first word alone overruns, because a row that cannot
|
|
474
|
-
* hold one word has no boundary to find. */
|
|
475
|
-
function wordCut(text, room) {
|
|
476
|
-
if (visibleWidth(text) <= room)
|
|
477
|
-
return text;
|
|
478
|
-
const hard = widthCut(text, Math.max(1, room - 1));
|
|
479
|
-
const at = hard.lastIndexOf(" ");
|
|
480
|
-
return `${at > room / 3 ? hard.slice(0, at) : hard}\u2026`;
|
|
481
|
-
}
|
|
482
|
-
/** Fold a line's CONTENT at W−2 and prefix EVERY row with the gutter
|
|
483
|
-
* (W2: a wrapped tool row keeps its state mark — the left edge alone
|
|
484
|
-
* distinguishes the states at --plain; the UserMessage rail precedent,
|
|
485
|
-
* v5 #16f). The gutter carries its own SGR (e.g. the bold ✓). W21:
|
|
486
|
-
* exported for the approval panel's text args (the same │ gutter). */
|
|
487
422
|
/**
|
|
488
423
|
* TUI2-R1.5 ⑨ (VD-10) — the WORD-aware fold, for text a human reads.
|
|
489
424
|
*
|
|
@@ -502,18 +437,6 @@ function wordCut(text, room) {
|
|
|
502
437
|
* overflowing row would violate invariant ①, and a word that cannot fit
|
|
503
438
|
* has to be broken somewhere.
|
|
504
439
|
*/
|
|
505
|
-
/** The SGR spans still open at the end of `text`, given those open at
|
|
506
|
-
* its start. A reset closes everything; anything else stacks. */
|
|
507
|
-
function spansOpenAfter(text, before) {
|
|
508
|
-
let open = [...before];
|
|
509
|
-
for (const m of text.matchAll(/\x1b\[[0-9;]*m/g)) {
|
|
510
|
-
if (m[0] === "\x1b[0m")
|
|
511
|
-
open = [];
|
|
512
|
-
else
|
|
513
|
-
open.push(m[0]);
|
|
514
|
-
}
|
|
515
|
-
return open;
|
|
516
|
-
}
|
|
517
440
|
export function foldWords(line, W) {
|
|
518
441
|
if (W < 1)
|
|
519
442
|
return [line];
|
|
@@ -659,25 +582,17 @@ function toolTargetOf(c) {
|
|
|
659
582
|
}
|
|
660
583
|
return toolTarget(c.name, input);
|
|
661
584
|
}
|
|
662
|
-
/**
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* The block's cut note keeps the RAW name (it names the tool the
|
|
674
|
-
* model should call again). W4: the settled row's parentheses hold
|
|
675
|
-
* the human metadata (settledMeta) — the input summary lived in the
|
|
676
|
-
* running row; the OUTCOME is what the settled row says. A4: the
|
|
677
|
-
* settled row keeps the TARGET — verb + target + outcome, the running
|
|
678
|
-
* row's summary column (the W19 pinned row keeps the full call name
|
|
679
|
-
* instead). A5: the verdict rides the head row — a decidedBy present
|
|
680
|
-
* on the cell appends `· approved by X` (extension auto-approvals) or
|
|
585
|
+
/** THE TOOL CARD — one per call, every state its own render (R13):
|
|
586
|
+
* pad · head row · blank · the preview, capped at CAP_PREVIEW rows and
|
|
587
|
+
* cut with a note that names the key · blank · the outcome row · pad;
|
|
588
|
+
* a call with nothing to show is the three-row bodiless card. The body
|
|
589
|
+
* is capped in SCREEN rows at the current width; the tool's OWN
|
|
590
|
+
* truncation note (W10, `capped by …`) is a different fact, never
|
|
591
|
+
* counted against the cap. W3: the verb drops its "_file" suffix and
|
|
592
|
+
* pads to 5 columns so the targets line up (the pipe path prints the
|
|
593
|
+
* same verb). A4: the settled head row is verb + target + outcome; the
|
|
594
|
+
* W19 pinned deny keeps the full call name instead. A5: a decidedBy on
|
|
595
|
+
* the cell appends `· approved by X` (an extension's auto-approval) or
|
|
681
596
|
* `· by X` on the pinned deny; the human decision needs no marker. */
|
|
682
597
|
class ToolExecution {
|
|
683
598
|
cell;
|
|
@@ -689,10 +604,9 @@ class ToolExecution {
|
|
|
689
604
|
const c = this.cell;
|
|
690
605
|
const verb = escapeTerminal(displayVerb(c.name));
|
|
691
606
|
const verbCol = verb.length < 5 ? `${verb}${" ".repeat(5 - verb.length)}` : verb;
|
|
692
|
-
// R13
|
|
693
|
-
// here, ahead of everything else a settled
|
|
694
|
-
// retired with the `rolled` field they read
|
|
695
|
-
// #foldOrRollup for the reversal in full).
|
|
607
|
+
// DECLARED REVERSAL (R13): the W13 rollup row and TUI2-R1 (B)'s
|
|
608
|
+
// exploration row stood here, ahead of everything else a settled
|
|
609
|
+
// call could be; both retired with the `rolled` field they read.
|
|
696
610
|
if (c.state === "done") {
|
|
697
611
|
// R3i phase 5: an answered (or declined) ask_user renders its
|
|
698
612
|
// OWN block — the questions and what the human said. The row
|
|
@@ -771,15 +685,6 @@ class ToolExecution {
|
|
|
771
685
|
// a pipe with the colour stripped. A failure keeps its colour
|
|
772
686
|
// AND its words — see settledMeta.
|
|
773
687
|
const body = toolBlockParts(c, W, ctx).rows;
|
|
774
|
-
if (body.length > 0 && c.expanded) {
|
|
775
|
-
// An EXPANDED block is already showing everything, and its own
|
|
776
|
-
// footer ("ctrl+o collapses") is what closes it. Giving it an
|
|
777
|
-
// outcome row as well would put two closing rows on one block
|
|
778
|
-
// and move the metadata off a head row every width gate pins.
|
|
779
|
-
// It takes the SURFACE and nothing else.
|
|
780
|
-
const head = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
781
|
-
return slabBlock(appendSuffix(head, expandSuffix(hidden, W - visibleWidth(head))), body, null, W);
|
|
782
|
-
}
|
|
783
688
|
if (body.length > 0) {
|
|
784
689
|
// R13 — THE CARD, when there is something to preview: the head
|
|
785
690
|
// row names the call, the preview sits inside, and the outcome
|
|
@@ -804,8 +709,34 @@ class ToolExecution {
|
|
|
804
709
|
// what happened and how long it took — is never cut open.
|
|
805
710
|
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
806
711
|
const attr = approvedBy.replace(/^ · /, "");
|
|
807
|
-
|
|
808
|
-
|
|
712
|
+
// DC-50 / R14: the key is RESERVED (§7.5), so its width comes
|
|
713
|
+
// out of the budget BEFORE the tiers are picked — not
|
|
714
|
+
// appended after, which is what the first build did and
|
|
715
|
+
// which let a narrow row cut the affordance off entirely.
|
|
716
|
+
// The tier ladder is what gives way; the key never is.
|
|
717
|
+
const keySuffix = c.expanded ? ` · ${COLLAPSE_ROW}` : "";
|
|
718
|
+
const words = pickTier([join(meta, counted, `${elapsed}s`, attr), join(meta, counted, `${elapsed}s`), join(meta, `${elapsed}s`), meta], W - visibleWidth(noteIndent()) - keySuffix.length);
|
|
719
|
+
// DC-50 / R14 — ONE CARD, ONE SKELETON, expanded or not.
|
|
720
|
+
//
|
|
721
|
+
// The expanded card used to take a DIFFERENT shape: its head
|
|
722
|
+
// row carried the outcome inline (`shell npm test · exit 0 ·
|
|
723
|
+
// 40 lines · 0.1s`), it had no outcome row at all, and the
|
|
724
|
+
// `ctrl+o collapses` affordance was a row of its own at the
|
|
725
|
+
// end of the body. That was defensible while an expanded card
|
|
726
|
+
// was rare — reachable only for a live or approval-parked
|
|
727
|
+
// cell — and it stopped being defensible the moment ctrl+o
|
|
728
|
+
// became a switch that expands EVERY settled card at once: a
|
|
729
|
+
// page where the same call has two skeletons depending on a
|
|
730
|
+
// global toggle is the instability this round is named for.
|
|
731
|
+
//
|
|
732
|
+
// So both states are pad · head · blank · body · blank ·
|
|
733
|
+
// outcome · pad. The head row says WHAT was run and nothing
|
|
734
|
+
// else; the outcome row says what happened, how much, how
|
|
735
|
+
// long — and carries the affordance, because the affordance
|
|
736
|
+
// is a fact about this card's state and the outcome row is
|
|
737
|
+
// where this card's facts live.
|
|
738
|
+
const withKey = `${words}${keySuffix}`;
|
|
739
|
+
const outcome = c.isError ? `${p.red}${withKey}${p.reset}` : withKey;
|
|
809
740
|
return slabBlock(head, body, outcome, W);
|
|
810
741
|
}
|
|
811
742
|
// R13 — nothing to preview: the SAME card, three rows, with the
|
|
@@ -863,7 +794,7 @@ class ToolExecution {
|
|
|
863
794
|
// there is no card — the call keeps its head row until it
|
|
864
795
|
// commits, which is the one form that fits anywhere.
|
|
865
796
|
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
866
|
-
const gutter =
|
|
797
|
+
const gutter = `${breathFrame(ctx.spinnerI)} `;
|
|
867
798
|
if (liveRows <= 0) {
|
|
868
799
|
// the degraded form is the head row ALONE, so it keeps the
|
|
869
800
|
// duration it would otherwise have lost with its card — cut
|
|
@@ -923,8 +854,8 @@ class ToolExecution {
|
|
|
923
854
|
* The affordance is a statement about hidden content: a cell whose body
|
|
924
855
|
* is already whole on screen must not advertise a key that would show it
|
|
925
856
|
* the same thing, and a cell that already carries its own renderer cut
|
|
926
|
-
* (
|
|
927
|
-
*
|
|
857
|
+
* (the `… N · ctrl+o expands` note) already teaches the key at the
|
|
858
|
+
* place the content stops. What is LEFT — and it is the
|
|
928
859
|
* common case — is every settled non-shell call, whose collapsed body is
|
|
929
860
|
* empty: the whole result sits behind the key with nothing on screen
|
|
930
861
|
* saying so.
|
|
@@ -1012,23 +943,13 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "
|
|
|
1012
943
|
const core = join(meta, counted, `${elapsed}s`);
|
|
1013
944
|
const withAttr = join(meta, counted, `${elapsed}s`, attr.replace(" · ", ""));
|
|
1014
945
|
const lead = `${verbCol} `;
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
return full;
|
|
1023
|
-
// 2. the attribution gives way
|
|
1024
|
-
const bare = fit(target, core);
|
|
1025
|
-
if (bare !== null)
|
|
1026
|
-
return bare;
|
|
1027
|
-
// 2b. the COUNT gives way next (pin 4), where the suffix is not
|
|
1028
|
-
// already carrying it
|
|
1029
|
-
const short = fit(target, join(meta, `${elapsed}s`));
|
|
1030
|
-
if (short !== null)
|
|
1031
|
-
return short;
|
|
946
|
+
const row = (tail) => `${lead}${target}${tail === "" ? "" : ` · ${tail}`}`;
|
|
947
|
+
// 1. everything; 2. the attribution gives way; 2b. the COUNT gives
|
|
948
|
+
// way next (pin 4), where the suffix is not already carrying it —
|
|
949
|
+
// the target whole through all three
|
|
950
|
+
const whole = firstFit([row(withAttr), row(core), row(join(meta, `${elapsed}s`))], room);
|
|
951
|
+
if (whole !== null)
|
|
952
|
+
return whole;
|
|
1032
953
|
// 3. the target truncates, the core stays whole
|
|
1033
954
|
const stem = join(meta, `${elapsed}s`);
|
|
1034
955
|
const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
|
|
@@ -1066,15 +987,12 @@ function attribution(c) {
|
|
|
1066
987
|
return "";
|
|
1067
988
|
return c.verdict.decision === "denied" ? " · denied" : " · approved";
|
|
1068
989
|
}
|
|
1069
|
-
|
|
990
|
+
/** The expand key as a row's tail, in two tiers; empty when there is
|
|
991
|
+
* nothing hidden. R13 moved the line COUNT off this suffix onto the
|
|
992
|
+
* head row's own `·` chain (VD-6: stated exactly once). */
|
|
993
|
+
function expandSuffix(lines, room) {
|
|
1070
994
|
if (lines === null)
|
|
1071
995
|
return "";
|
|
1072
|
-
// R13 — the COUNT left this suffix for the head row's own `·` chain,
|
|
1073
|
-
// where the bodied card keeps it too (`… · 10 lines · 0.1s`). It was
|
|
1074
|
-
// here because the parenthesised core had nowhere to put it and the
|
|
1075
|
-
// suffix was the only tail the row had; with one grammar for both
|
|
1076
|
-
// cards there is one place, and VD-6's "stated exactly once" is what
|
|
1077
|
-
// forbids leaving a copy behind.
|
|
1078
996
|
for (const tier of [" · ctrl+o expands", " · ctrl+o"]) {
|
|
1079
997
|
if (tier.length <= room)
|
|
1080
998
|
return tier;
|
|
@@ -1089,186 +1007,43 @@ function appendSuffix(row, suffix) {
|
|
|
1089
1007
|
const p = palette();
|
|
1090
1008
|
return `${row}${p.dim}${suffix}${p.reset}`;
|
|
1091
1009
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
* row. Zero new rows, zero new columns: the affordance the cell already
|
|
1099
|
-
* prints is the marker.
|
|
1100
|
-
*
|
|
1101
|
-
* Applied to a row rather than composed into it on purpose. The token is
|
|
1102
|
-
* emitted from several places (the settled suffix, the renderer's own
|
|
1103
|
-
* `└ +N … · ctrl+o` cut rows) and threading a flag through all of them
|
|
1104
|
-
* would put the invariant "exactly one bright token" in as many hands as
|
|
1105
|
-
* there are emitters. Here it has exactly one.
|
|
1106
|
-
*
|
|
1107
|
-
* NO_COLOR: p.dim is empty, so the row's bytes are untouched.
|
|
1108
|
-
*/
|
|
1109
|
-
export function focusToken(row, W) {
|
|
1110
|
-
const p = palette();
|
|
1111
|
-
const at = row.lastIndexOf(EXPAND_KEY);
|
|
1112
|
-
if (at !== -1) {
|
|
1113
|
-
// the row already names the key — brighten the token in place, and
|
|
1114
|
-
// leave every other span exactly as it was
|
|
1115
|
-
if (p.dim === "")
|
|
1116
|
-
return row;
|
|
1117
|
-
// DC-3: the marker takes the WASH. It used to take the inline-code
|
|
1118
|
-
// tint and inherited its 1.54:1 — the cue naming the key that
|
|
1119
|
-
// reveals a cell was itself the least readable thing on a white
|
|
1120
|
-
// terminal. The wash is right for a second reason: this token has
|
|
1121
|
-
// to be UNIQUE on the frame ("exactly one bright token"), and an
|
|
1122
|
-
// attribute like bold is spent everywhere. It closes with washEnd
|
|
1123
|
-
// rather than a reset, so the surrounding dim survives instead of
|
|
1124
|
-
// having to be re-applied.
|
|
1125
|
-
return `${row.slice(0, at)}${p.lift}${EXPAND_KEY}${p.dim}${row.slice(at + EXPAND_KEY.length)}`;
|
|
1126
|
-
}
|
|
1127
|
-
// A LIVE row does not carry the affordance today, and the live cell is
|
|
1128
|
-
// the one ctrl+o takes FIRST (expandNext scans the live tail before
|
|
1129
|
-
// the committed ring) — so the row the key is aimed at was the one row
|
|
1130
|
-
// that never said the key existed. The affordance IS the marker here:
|
|
1131
|
-
// it appears on the focused row and nowhere else, which is why no
|
|
1132
|
-
// unfocused row's bytes move (every existing live-row assertion
|
|
1133
|
-
// renders a cell with no focus and is untouched).
|
|
1134
|
-
const room = W - visibleWidth(row);
|
|
1135
|
-
if (room < SUFFIX_MIN)
|
|
1136
|
-
return row; // never at the cost of invariant ①
|
|
1137
|
-
return `${row}${p.dim} · ${p.lift}${EXPAND_KEY}${p.reset}`;
|
|
1138
|
-
}
|
|
1139
|
-
/** DC-41 — the label in ONE place. The key has moved once now, and
|
|
1140
|
-
* a constant named after its binding is a comment that lies. */
|
|
1141
|
-
const EXPAND_KEY = "ctrl+o";
|
|
1142
|
-
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
1143
|
-
* rollup's expanded list carries a second clause (its members' full
|
|
1144
|
-
* outputs live in /last, which the group row cannot show). */
|
|
1010
|
+
/* DECLARED REVERSAL (D-S2-1, owner-ruled 2026-09-06): `focusToken` and
|
|
1011
|
+
`EXPAND_KEY` stood here — TUI2-R2 ⑤'s bright ctrl+o token on the
|
|
1012
|
+
newest live card, "exactly one bright token per frame". DC-50 made
|
|
1013
|
+
ctrl+o a global switch, so there was no target left for a marker to
|
|
1014
|
+
name; the status row's idle hint names the switch (`idleHint`). */
|
|
1015
|
+
/** TUI2-R1 (A) — the expanded card's key suffix: the way back. */
|
|
1145
1016
|
const COLLAPSE_ROW = "ctrl+o collapses";
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
* the plurals for its other-tool terms ("2 dirs", "1 match"). */
|
|
1151
|
-
export const ROLLUP_NOUN = {
|
|
1152
|
-
read_file: "files",
|
|
1153
|
-
list_dir: "dirs",
|
|
1154
|
-
search_text: "matches",
|
|
1155
|
-
};
|
|
1156
|
-
// ---- TUI2-R1 (B): the exploration rollup ----
|
|
1157
|
-
/** TUI2-R1 (B) — the exploration row's nouns. Deliberately NOT
|
|
1158
|
-
* ROLLUP_NOUN: that table says what a SINGLE-tool rollup counts
|
|
1159
|
-
* ("5 matches"), and this row counts CALLS across tools, where
|
|
1160
|
-
* "14 searches" is what happened. Both tables stay — changing the
|
|
1161
|
-
* older one would move an assertion this round did not declare. */
|
|
1162
|
-
const EXPLORE_NOUN = {
|
|
1163
|
-
read_file: ["file", "files"],
|
|
1164
|
-
list_dir: ["dir", "dirs"],
|
|
1165
|
-
search_text: ["search", "searches"],
|
|
1166
|
-
};
|
|
1167
|
-
/** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
|
|
1168
|
-
* TUI2-R2pre ④: this used to be a private three-tool table saying the
|
|
1169
|
-
* same thing as the card head's `_file` strip, in a different way and
|
|
1170
|
-
* for a different set of tools. Both are `displayVerb` now — the whole
|
|
1171
|
-
* point of the ruling is that there is ONE answer to "what does the
|
|
1172
|
-
* screen call this". The cut note, which used to be the deliberate
|
|
1173
|
-
* exception here, moved with it (see toolCutNote). */
|
|
1174
|
-
/** Whether a tool joins an exploration run. Exactly the read-only set —
|
|
1175
|
-
* writes, edits, shells and extension tools never group (a burst of
|
|
1176
|
-
* side effects is a list of things that HAPPENED, and every row of it
|
|
1177
|
-
* carries meaning). */
|
|
1178
|
-
export function isExploreTool(name) {
|
|
1179
|
-
return EXPLORE_NOUN[name] !== undefined;
|
|
1180
|
-
}
|
|
1181
|
-
/** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
|
|
1182
|
-
* scrollback, becomes ONE line — the work order's claimed shape
|
|
1183
|
-
* (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
|
|
1184
|
-
* toolStart: read_file → "reads", edit_file → "edits", the other tools
|
|
1185
|
-
* as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
|
|
1186
|
-
* in, the verb + "s" otherwise).
|
|
1187
|
-
* A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
1188
|
-
* words LEAD the one line, `✦ <chip> · thought 19s · read 5 files` —
|
|
1189
|
-
* the chip the SAME SGR-7 bracket as the live user row (#16f, side
|
|
1190
|
-
* pads included). The words take the fold's width budget and width-cut
|
|
1191
|
-
* at the end with the honest "…" (never a silent truncate — invariant
|
|
1192
|
-
* ① holds on the ONE row by construction).
|
|
1193
|
-
*
|
|
1194
|
-
* DECLARED SUPERSESSION (R3g, 2026-08-28) — A9 also ruled that "the
|
|
1195
|
-
* metadata terms give way LAST". They do not any more: the KEY does.
|
|
1196
|
-
* A9 was taken when this line carried no key, and at a width where the
|
|
1197
|
-
* chip, the full metadata and " · ctrl+o" cannot coexist, a fold with
|
|
1198
|
-
* no key is the turn's work behind a line with no way back to it. So
|
|
1199
|
-
* the order is now words, then metadata, then — never — the key. The
|
|
1200
|
-
* glyph is ✦ and the zero terms are dropped (R3b), so the example above
|
|
1201
|
-
* is written as the code renders it rather than as A9 first wrote it. */
|
|
1202
|
-
/**
|
|
1203
|
-
* R3b — what a run of work DID, in words. One definition, because two
|
|
1204
|
-
* surfaces say it: the fold line (`turnFold`, above) and the expand
|
|
1205
|
-
* header the compositor writes when that fold is opened. A second copy
|
|
1206
|
-
* would be a second answer to the same question, and the first thing to
|
|
1207
|
-
* drift would be the plurals — `search_text` is "matches", not
|
|
1208
|
-
* "searchs", and only the ROLLUP_NOUN table knows that.
|
|
1209
|
-
*
|
|
1210
|
-
* Zero terms are dropped (owner ruling, R3b): a term earns its place by
|
|
1211
|
-
* having a count.
|
|
1212
|
-
*/
|
|
1213
|
-
/**
|
|
1214
|
-
* R3g (2026-08-28) — the fold's own terms, VERB + COUNT + NOUN.
|
|
1215
|
-
*
|
|
1216
|
-
* DECLARED SUPERSESSION. R3b built these terms out of ROLLUP_NOUN,
|
|
1217
|
-
* whose own comment says not to: that table names what a single-tool
|
|
1218
|
-
* rollup COUNTS ("5 matches" — five matched lines), and this line
|
|
1219
|
-
* counts CALLS. So one search_text call rendered `1 match`, a sentence
|
|
1220
|
-
* that is false whenever the search matched any other number — which is
|
|
1221
|
-
* almost always. `shell` fell through to the verb branch and read
|
|
1222
|
-
* `4 shells`.
|
|
1223
|
-
*
|
|
1224
|
-
* The phrasing is the owner's, from the shape they asked for:
|
|
1225
|
-
* "thought 17s · read 4 files · listed 1 directory · ran 4 shell
|
|
1226
|
-
* commands". A tool with no entry says `3 × <verb>`, which counts calls
|
|
1227
|
-
* without inventing a noun for them.
|
|
1228
|
-
*/
|
|
1017
|
+
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): the W13 rollup's
|
|
1018
|
+
noun table (`ROLLUP_NOUN`) and TUI2-R1 (B)'s exploration-run set
|
|
1019
|
+
(`EXPLORE_NOUN`, `isExploreTool`) stood here. Both collapses retired
|
|
1020
|
+
with the fold; the verb column is `displayVerb` for every tool. */
|
|
1229
1021
|
/**
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1022
|
+
* THE TERM TABLE — [past, singular, plural], one row per tool the recap
|
|
1023
|
+
* names (R3g's phrasing, the owner's: "thought 17s · read 4 files ·
|
|
1024
|
+
* listed 1 directory · ran 4 shell commands"). A tool with no entry
|
|
1025
|
+
* says `3 × <verb>`, which counts calls without inventing a noun for
|
|
1026
|
+
* them; a zero term is dropped (R3b: a term earns its place by having a
|
|
1027
|
+
* count). The terms count CALLS, which is what the CLI hands them.
|
|
1028
|
+
*
|
|
1029
|
+
* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): this table served
|
|
1030
|
+
* the folded-turn line (W14, `turnFold`) and R3i's stretch line, and
|
|
1031
|
+
* carried a PROGRESSIVE column so the live tense and the settled tense
|
|
1032
|
+
* could not drift apart. Nothing folds and nothing reads the
|
|
1033
|
+
* progressive; the recap (`foldTerms`) is the table's only reader.
|
|
1034
|
+
* R3h's object-vs-act distinction (`foldCountsObjects`) retired with
|
|
1035
|
+
* the fold that consumed it.
|
|
1242
1036
|
*/
|
|
1243
1037
|
const TERM = {
|
|
1244
|
-
read_file: ["read", "
|
|
1245
|
-
edit_file: ["edited", "
|
|
1246
|
-
write_file: ["wrote", "
|
|
1247
|
-
list_dir: ["listed", "
|
|
1248
|
-
search_text: ["ran", "
|
|
1249
|
-
shell: ["ran", "
|
|
1038
|
+
read_file: ["read", "file", "files"],
|
|
1039
|
+
edit_file: ["edited", "file", "files"],
|
|
1040
|
+
write_file: ["wrote", "file", "files"],
|
|
1041
|
+
list_dir: ["listed", "directory", "directories"],
|
|
1042
|
+
search_text: ["ran", "search", "searches"],
|
|
1043
|
+
shell: ["ran", "shell command", "shell commands"],
|
|
1250
1044
|
};
|
|
1251
|
-
const FOLD_TERM = Object.fromEntries(Object.entries(TERM).map(([name, [past, , singular, plural]]) => [name, [past, singular, plural]]));
|
|
1252
|
-
/**
|
|
1253
|
-
* R3h (fable, 2026-08-29) — WHICH TERMS COUNT OBJECTS.
|
|
1254
|
-
*
|
|
1255
|
-
* The rule, one sentence: a term that counts OBJECTS counts distinct
|
|
1256
|
-
* objects; a term that counts ACTS counts calls. `read 2 files` after
|
|
1257
|
-
* reading ONE file twice is a false sentence, and law 1.3 does not
|
|
1258
|
-
* become optional because the falsehood is small. `ran 2 searches`
|
|
1259
|
-
* after searching the same pattern twice is TRUE — the acts happened.
|
|
1260
|
-
*
|
|
1261
|
-
* The table sits beside FOLD_TERM so the two cannot drift: a tool whose
|
|
1262
|
-
* noun is a thing ("files", "directories") belongs here; a tool whose
|
|
1263
|
-
* noun is an act ("searches", "shell commands") does not.
|
|
1264
|
-
*/
|
|
1265
|
-
const FOLD_COUNTS_OBJECTS = new Set(["read_file", "edit_file", "write_file", "list_dir"]);
|
|
1266
|
-
/** Does this tool's fold term count distinct targets rather than calls? */
|
|
1267
|
-
export function foldCountsObjects(name) {
|
|
1268
|
-
return FOLD_COUNTS_OBJECTS.has(name);
|
|
1269
|
-
}
|
|
1270
1045
|
function foldTerm(name, n) {
|
|
1271
|
-
const t =
|
|
1046
|
+
const t = TERM[name];
|
|
1272
1047
|
if (t === undefined)
|
|
1273
1048
|
return `${n} × ${displayVerb(name)}`;
|
|
1274
1049
|
return `${t[0]} ${n} ${n === 1 ? t[1] : t[2]}`;
|
|
@@ -1286,41 +1061,6 @@ export function foldTerms(reads, edits, others) {
|
|
|
1286
1061
|
}
|
|
1287
1062
|
return parts;
|
|
1288
1063
|
}
|
|
1289
|
-
/**
|
|
1290
|
-
* R3i — THE STRETCH LINE: the turn's one working row, in three phases.
|
|
1291
|
-
*
|
|
1292
|
-
* A STRETCH is the run of thinking and tool calls between two blocks of
|
|
1293
|
-
* the model's prose. While it runs it is this line plus a bounded act
|
|
1294
|
-
* window; when it closes it commits as this same line, frozen, with its
|
|
1295
|
-
* key. The contract in one sentence: **the line you watch is the line
|
|
1296
|
-
* you keep** — the settle changes the mark, the tense and the key, and
|
|
1297
|
-
* nothing else.
|
|
1298
|
-
*
|
|
1299
|
-
* thinking ✧ thinking 4s
|
|
1300
|
-
* acting ✶ reading 6 files · running 4 shell commands
|
|
1301
|
-
* settled ✦ thought 9s · read 6 files · ran 4 shell commands · ctrl+o
|
|
1302
|
-
*
|
|
1303
|
-
* THE GIVE-WAY LADDER, in order, because at some width everything
|
|
1304
|
-
* cannot fit:
|
|
1305
|
-
*
|
|
1306
|
-
* 1. the human's WORDS (the A9 chip on a quiet turn) — they are on
|
|
1307
|
-
* screen above, in the chip band;
|
|
1308
|
-
* 2. the NOUNS compact, cheapest word first, and stop as soon as the
|
|
1309
|
-
* row fits — buying one cell must not spend every substitution;
|
|
1310
|
-
* 3. the COUNTS cut, with the honest "…";
|
|
1311
|
-
* 4. the TROUBLE CLAUSE cuts. The design first said it never gives
|
|
1312
|
-
* way, and that was unimplementable: a long clause overflows after
|
|
1313
|
-
* the counts have already cut to a bare "…", and invariant ①
|
|
1314
|
-
* throws on that row;
|
|
1315
|
-
* 5. the KEY gives way NEVER. A fold with no key is the turn's work
|
|
1316
|
-
* behind a line with no way back to it, which is the one thing
|
|
1317
|
-
* this row must not be.
|
|
1318
|
-
*
|
|
1319
|
-
* `…` in this file means CUT HERE and nothing else — which is why the
|
|
1320
|
-
* live phases carry no trailing ellipsis for in-flight, though the
|
|
1321
|
-
* reference implementation uses one. The moving mark and the present
|
|
1322
|
-
* tense already say it twice.
|
|
1323
|
-
*/
|
|
1324
1064
|
/**
|
|
1325
1065
|
* R3i phase 5 — THE ANSWERED QUESTION'S BLOCK.
|
|
1326
1066
|
*
|
|
@@ -1338,7 +1078,7 @@ export function foldTerms(reads, edits, others) {
|
|
|
1338
1078
|
* is emphasis, never information). A typed answer says `(typed)`,
|
|
1339
1079
|
* because where an answer came from is a fact about it.
|
|
1340
1080
|
*
|
|
1341
|
-
* It is WORDS, not work (law 1.7):
|
|
1081
|
+
* It is WORDS, not work (law 1.7): no summary ever stands for it,
|
|
1342
1082
|
* because the one thing a summary must not do is speak for the human.
|
|
1343
1083
|
*
|
|
1344
1084
|
* A result that is not the ask's own JSON yields NOTHING. This renderer
|
|
@@ -1376,45 +1116,11 @@ export function askedBlock(resultText, seconds, W) {
|
|
|
1376
1116
|
}),
|
|
1377
1117
|
];
|
|
1378
1118
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
];
|
|
1385
|
-
/** R3i — a stretch of exactly ONE call names its TARGET instead of its
|
|
1386
|
-
* count. `thought 2s · read 1 file` replaces two rows — the thinking
|
|
1387
|
-
* and the call — with a row that says less than either of them did,
|
|
1388
|
-
* and "thinking plus one call" is the commonest shape a narrating
|
|
1389
|
-
* model makes. This is the answer to the defect R3d killed R3b's
|
|
1390
|
-
* per-segment folds over; the "absorbs at least two rows" rule alone
|
|
1391
|
-
* does not answer it. */
|
|
1392
|
-
function stretchTerms(t, live) {
|
|
1393
|
-
// R4 — the tense is per TERM. A name with nothing in flight is in the
|
|
1394
|
-
// past whatever the line's own phase is; with liveNames absent (the
|
|
1395
|
-
// settled line) every term follows the line.
|
|
1396
|
-
const tense = (name) => (live && (t.liveNames === undefined || t.liveNames.includes(name)) ? 1 : 0);
|
|
1397
|
-
const total = t.calls.reduce((n, [, c]) => n + c, 0);
|
|
1398
|
-
// R4 — the one-call TARGET form is the SETTLED line's. Live, the act
|
|
1399
|
-
// slot directly below already names the target on its head row, so
|
|
1400
|
-
// the line was printing the same words twice, one above the other
|
|
1401
|
-
// (`running npm run check` over `shell npm run check`). Settled there
|
|
1402
|
-
// is no slot, and naming the target is strictly more than counting to
|
|
1403
|
-
// one — which is the R3i rule this keeps, where it applies.
|
|
1404
|
-
if (total === 1 && t.targets.length === 1 && !live) {
|
|
1405
|
-
const [name] = t.calls[0];
|
|
1406
|
-
const e = TERM[name];
|
|
1407
|
-
return [`${e === undefined ? name : e[0]} ${t.targets[0]}`];
|
|
1408
|
-
}
|
|
1409
|
-
return t.calls
|
|
1410
|
-
.filter(([, n]) => n > 0)
|
|
1411
|
-
.map(([name, n]) => {
|
|
1412
|
-
const e = TERM[name];
|
|
1413
|
-
if (e === undefined)
|
|
1414
|
-
return `${n} × ${displayVerb(name)}`;
|
|
1415
|
-
return `${e[tense(name)]} ${n} ${n === 1 ? e[2] : e[3]}`;
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1119
|
+
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): the stretch line's
|
|
1120
|
+
term machinery stood here — `StretchTerms`, `STRETCH_COMPACT`,
|
|
1121
|
+
`stretchTerms` (R3i's one-line-per-stretch, R4's per-term tense).
|
|
1122
|
+
The line retired with the fold; the TERM table above survives for the
|
|
1123
|
+
turn's recap alone. */
|
|
1418
1124
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1419
1125
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
1420
1126
|
* (the W7 table). The renderer-cut row is inside the cap. */
|
|
@@ -1424,14 +1130,7 @@ export const CAP_PREVIEW = 5;
|
|
|
1424
1130
|
/** DC-46 — the running window's ceiling is the SETTLED preview's, and a
|
|
1425
1131
|
* running card reaches it by growing rather than by being handed it.
|
|
1426
1132
|
* `LIVE_WINDOW` (CAP_PREVIEW + 1) retires with the allocation it sized. */
|
|
1427
|
-
/** The rows a card costs besides its window: two pads, the head, two
|
|
1428
|
-
* blanks and the status row. Below this there is no card (DC-43). */
|
|
1429
|
-
export const CARD_CHROME = 6;
|
|
1430
1133
|
const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
1431
|
-
/** The block body rows' prefixes (W2's gutter table): │ a bounded
|
|
1432
|
-
* block's body, └ the block's last row — what was cut, where the rest
|
|
1433
|
-
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
1434
|
-
* distinguishes the states at --plain). Structural (constraint 1). */
|
|
1435
1134
|
/** R8a — A TOOL BLOCK'S ROWS ARE INDENTED, NOT GUTTERED.
|
|
1436
1135
|
*
|
|
1437
1136
|
* `│ ` on every row drew a bar down the left of every multi-row
|
|
@@ -1445,8 +1144,7 @@ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
|
1445
1144
|
* row (see openBlock). In-block notes take the same indent,
|
|
1446
1145
|
* no glyph — because a second `└` inside one block would be the same
|
|
1447
1146
|
* mark meaning two things (§4.1). CUT_ROW is unchanged for the
|
|
1448
|
-
* surfaces that are not a tool block
|
|
1449
|
-
* slot's overflow count. */
|
|
1147
|
+
* surfaces that are not a tool block. */
|
|
1450
1148
|
/** R8a's four columns — off the surface. Inside a painted card every
|
|
1451
1149
|
* row sits at column 2 (R13 E4): the head row and the outcome row
|
|
1452
1150
|
* bracket the preview, so the indent is no longer what says "these
|
|
@@ -1516,10 +1214,18 @@ function slabRow(inner, W) {
|
|
|
1516
1214
|
* metadata rows: the parts give way in a PINNED ORDER, and the part
|
|
1517
1215
|
* that carries the semantics is the one reserved. */
|
|
1518
1216
|
function pickTier(tiers, room) {
|
|
1217
|
+
return firstFit(tiers, room) ?? tiers[tiers.length - 1];
|
|
1218
|
+
}
|
|
1219
|
+
/** The widest form that fits the row, or null when none does — the
|
|
1220
|
+
* settled head row asks this with its target whole, and only then
|
|
1221
|
+
* cuts the target. The three rows that give way in a pinned order
|
|
1222
|
+
* (the settled head, the running head, the outcome row) share this
|
|
1223
|
+
* walk; their ORDERS differ by ruling and stay their own. */
|
|
1224
|
+
function firstFit(tiers, room) {
|
|
1519
1225
|
for (const t of tiers)
|
|
1520
1226
|
if (visibleWidth(t) <= room)
|
|
1521
1227
|
return t;
|
|
1522
|
-
return
|
|
1228
|
+
return null;
|
|
1523
1229
|
}
|
|
1524
1230
|
function noteRow(text, W, tone) {
|
|
1525
1231
|
const p = palette();
|
|
@@ -1570,30 +1276,20 @@ function slabBlock(head, body, outcome, W) {
|
|
|
1570
1276
|
return [...top, slabRow("", W)];
|
|
1571
1277
|
return [...top, slabRow("", W), ...noteRow(outcome, W, "body").map((r) => slabRow(r, W)), slabRow("", W)];
|
|
1572
1278
|
}
|
|
1573
|
-
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
* `ctrl+o` used to append bare ground under a `✦` — the turn recap's own
|
|
1577
|
-
* mark, one symbol for two meanings (§4.1) — in a page where every other
|
|
1578
|
-
* piece of machine work is a card. And it lands after the recap, so the
|
|
1579
|
-
* only tie to the call it came from was that mark's sentence.
|
|
1279
|
+
/*
|
|
1280
|
+
* RETIRED (DC-50 / R14, 2026-09-05) — `expandedCard`.
|
|
1580
1281
|
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1282
|
+
* It drew the APPENDED block the old ctrl+o produced: a card printed far
|
|
1283
|
+
* below the call it belonged to, which is why its head row had to carry
|
|
1284
|
+
* `expanded · N turns back` and why its body carried section headers
|
|
1285
|
+
* around the raw input and output. All of that was addressing — a way
|
|
1286
|
+
* for a copy to say which original it was a copy of.
|
|
1584
1287
|
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1288
|
+
* DC-50 removes the copy. An expanded card is the ordinary `toolCard`
|
|
1289
|
+
* with `expanded` set, rendered where the call stands, so nothing needs
|
|
1290
|
+
* to say where it came from. Its shape is gated in
|
|
1291
|
+
* `r13-rhythm-surface.test.ts`.
|
|
1587
1292
|
*/
|
|
1588
|
-
export function expandedCard(verb, target, meta, sections, outcome, W) {
|
|
1589
|
-
const p = palette();
|
|
1590
|
-
const head = cutLine(` ${verb} ${p.bold}${escapeTerminal(target)}${p.reset}${slabPaints() ? p.washDim : p.dim} · ${escapeTerminal(meta)}${slabPaints() ? p.washDimEnd : p.reset}`, W);
|
|
1591
|
-
const body = [];
|
|
1592
|
-
for (const raw of sections)
|
|
1593
|
-
for (const row of blockRows(raw, W, slabPaints() ? "body" : "dim"))
|
|
1594
|
-
body.push(row);
|
|
1595
|
-
return slabBlock(head, body, outcome, W);
|
|
1596
|
-
}
|
|
1597
1293
|
/** 0.24.2 ② — the live region's `thinking…` placeholder: dim italic at
|
|
1598
1294
|
* column 2, no glyph, the SAME shape a thinking paragraph takes so that
|
|
1599
1295
|
* whatever arrives replaces it in place. Never committed — see the
|
|
@@ -1687,7 +1383,13 @@ function toolBlockParts(c, W, ctx) {
|
|
|
1687
1383
|
// rides a block that HAS rows — an expanded delegate whose summary
|
|
1688
1384
|
// marker is missing renders nothing, and a lone footer under a head
|
|
1689
1385
|
// row would be an affordance for an empty block.
|
|
1690
|
-
|
|
1386
|
+
//
|
|
1387
|
+
// DC-50 / R14: a SETTLED card carries the affordance on its OUTCOME
|
|
1388
|
+
// row instead, so that both states have one skeleton. This footer is
|
|
1389
|
+
// for the states that have no outcome row to carry it — a card parked
|
|
1390
|
+
// for approval, or one still running that was expanded before it
|
|
1391
|
+
// settled.
|
|
1392
|
+
if (c.expanded && rows.length > 0 && c.state !== "done")
|
|
1691
1393
|
rows.push(...foldLine(`${p.dim}${noteIndent()}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1692
1394
|
// R9 P2: `└` opens a block that has no surface. Inside a slab the
|
|
1693
1395
|
// surface IS the container, and a corner in it is §1.3's empty mark
|
|
@@ -1861,91 +1563,12 @@ function liveCap(c, room) {
|
|
|
1861
1563
|
liveHighWater.set(c, cap);
|
|
1862
1564
|
return cap;
|
|
1863
1565
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
*
|
|
1871
|
-
* R3i built the same window INTERMITTENTLY — a running call got its
|
|
1872
|
-
* fixed 1+3 block (W8), a finished one got nothing — so the live
|
|
1873
|
-
* region's height was a function of how many calls happened to be in
|
|
1874
|
-
* flight this frame. Over one real stretch that is 2 rows, then 7,
|
|
1875
|
-
* then 2, then 17 for a three-call batch, then 2 again, and every
|
|
1876
|
-
* transition scrolls everything above it. The owner's report was that
|
|
1877
|
-
* the screen "keeps jumping", and it was an accurate description of
|
|
1878
|
-
* the design, not a defect in its execution.
|
|
1879
|
-
*
|
|
1880
|
-
* The cure is not a smaller window, it is a STANDING one: between two
|
|
1881
|
-
* calls the slot keeps the call that just finished rather than
|
|
1882
|
-
* collapsing, and before any call it keeps the thinking that is
|
|
1883
|
-
* producing them — which is R3i ruling 5 ("thinking belongs on the
|
|
1884
|
-
* stretch line, IN THE ACT WINDOW, and in full in expansions") finally
|
|
1885
|
-
* wired, since R3i stated it while building no window for the thinking
|
|
1886
|
-
* phase to live in.
|
|
1887
|
-
*
|
|
1888
|
-
* Four rows, deliberately the same 1+3 shape W8 gave a running call, so
|
|
1889
|
-
* the commonest frame — exactly one call in flight — renders byte-for-
|
|
1890
|
-
* byte what 0.17.0 shipped.
|
|
1891
|
-
*/
|
|
1892
|
-
export const ACT_SLOT_ROWS = 4;
|
|
1893
|
-
/**
|
|
1894
|
-
* R4 — the slot's body rows: the tail of `text`, newest at the BOTTOM,
|
|
1895
|
-
* bottom-padded to exactly `rows`.
|
|
1896
|
-
*
|
|
1897
|
-
* The same dim │ gutter a running call's window uses (W2's table), and
|
|
1898
|
-
* the same two VD-4 rules: leading blank gutters are skipped, and the
|
|
1899
|
-
* short-output pad goes at the BOTTOM so output starts under its own
|
|
1900
|
-
* header and grows downward. The slot's CONTENTS change; its shape
|
|
1901
|
-
* does not.
|
|
1902
|
-
*/
|
|
1903
|
-
export function slotTail(text, W, rows) {
|
|
1904
|
-
if (rows <= 0)
|
|
1905
|
-
return [];
|
|
1906
|
-
const p = palette();
|
|
1907
|
-
const all = blockRows(text, W);
|
|
1908
|
-
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1909
|
-
const body = from < 0 ? [] : all.slice(from);
|
|
1910
|
-
// R7a: no pad. The slot stopped padding (see slotPad) and this was
|
|
1911
|
-
// the same pad by another route — three blank rows under a call with
|
|
1912
|
-
// nothing to say yet, which is the hole a7's blank-run guard prices.
|
|
1913
|
-
// R8a: the corner opens whatever slice survives the cap.
|
|
1914
|
-
return openBlock(body.slice(Math.max(0, body.length - rows)));
|
|
1915
|
-
}
|
|
1916
|
-
/** R4 — clamp or pad assembled slot rows to EXACTLY `rows`. The padding
|
|
1917
|
-
* is what makes the slot stand; the clamp is what keeps the slot from
|
|
1918
|
-
* ever being the thing that trips the force-commit cap (a slot that
|
|
1919
|
-
* could overflow would commit real cells to relieve blank rows). */
|
|
1920
|
-
export function slotPad(content, rows) {
|
|
1921
|
-
if (rows <= 0)
|
|
1922
|
-
return [];
|
|
1923
|
-
// R7a — THE SLOT NO LONGER PADS. It caps, and that is all.
|
|
1924
|
-
//
|
|
1925
|
-
// R4 padded to a fixed height because the slot's content came and
|
|
1926
|
-
// went: a finished call left the block, the block shrank, and every
|
|
1927
|
-
// row above it moved. The pad bought stability with rows drawn as
|
|
1928
|
-
// `│`, which is why a tall empty gutter ran down the screen under
|
|
1929
|
-
// every short block — the owner's own screenshot, and law 1.3's
|
|
1930
|
-
// case: a mark on a row with nothing to mark.
|
|
1931
|
-
//
|
|
1932
|
-
// Blanking the gutter revealed the hole it had been covering, and
|
|
1933
|
-
// the a7 replay priced the hole: blank runs over 2 in 653 of 733
|
|
1934
|
-
// frames, the screen never durably filling. So the pad had to go —
|
|
1935
|
-
// and the height it was buying is now bought by the CONTENT, since
|
|
1936
|
-
// R7a keeps every call's row for the life of the stretch. A block
|
|
1937
|
-
// whose rows only accumulate cannot shrink, so there is nothing
|
|
1938
|
-
// left for a pad to hold up. Measured: 65 of 733 at 40x24, the
|
|
1939
|
-
// pre-R7a number exactly, with the motion gates still green.
|
|
1940
|
-
return content.slice(0, rows);
|
|
1941
|
-
}
|
|
1942
|
-
/** R4 — the slot's overflow row: the calls in flight beyond the head
|
|
1943
|
-
* budget. It lives INSIDE the slot (it is one of the four rows), which
|
|
1944
|
-
* is what keeps a parallel burst from growing the region. */
|
|
1945
|
-
export function moreRunningRow(n, W) {
|
|
1946
|
-
const p = palette();
|
|
1947
|
-
return cutLine(` ${p.dim}${CUT_ROW}+${n} more running${p.reset}`, W);
|
|
1948
|
-
}
|
|
1566
|
+
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): R4's standing act
|
|
1567
|
+
slot stood here — `ACT_SLOT_ROWS`, `slotTail`, `slotPad`,
|
|
1568
|
+
`moreRunningRow` — a fixed four-row region that held the stretch's
|
|
1569
|
+
running call so the live region's height would not follow the call
|
|
1570
|
+
count. Every call is its own card now, allocated at its own height
|
|
1571
|
+
(E2, DC-46), so there is no slot for a call to occupy. */
|
|
1949
1572
|
/** W12: the delegate's child sessions collapse to the tool row plus ONE
|
|
1950
1573
|
* line — the height NEVER changes (running → settled replaces the row
|
|
1951
1574
|
* in place). The running row derives from the INPUT: the parent has no
|
|
@@ -2146,45 +1769,6 @@ class Banner {
|
|
|
2146
1769
|
* done-collapse. Every live row CUTS at W (never folds) — the block's
|
|
2147
1770
|
* height is its row count. */
|
|
2148
1771
|
export const CAP_TASK_LIVE = 6;
|
|
2149
|
-
/** W20 — the live block's fixed-window row cut: an SGR-aware ONE-ROW
|
|
2150
|
-
* truncation (foldLine wraps; a wrapped row would break the height
|
|
2151
|
-
* cap — every live row is exactly one screen row at every width).
|
|
2152
|
-
* A line that fits (≤ W) passes through whole; an overflow cuts the
|
|
2153
|
-
* content at W−1 — the ellipsis's slot — and the ellipsis rides AFTER
|
|
2154
|
-
* the reset (post-reset — the PTY needles' convention). The cut row
|
|
2155
|
-
* never exceeds W (invariant ①). W21: exported for the approval
|
|
2156
|
-
* panel's single-row lines (the rule line, the title, the divider,
|
|
2157
|
-
* the options/affordance rows). */
|
|
2158
|
-
export function cutLine(line, W) {
|
|
2159
|
-
if (visibleWidth(line) <= W)
|
|
2160
|
-
return line;
|
|
2161
|
-
let out = "";
|
|
2162
|
-
let width = 0;
|
|
2163
|
-
for (let i = 0; i < line.length;) {
|
|
2164
|
-
if (line[i] === "\x1b") {
|
|
2165
|
-
// exec returns an ARRAY — copying m coerces it (the match), but
|
|
2166
|
-
// m.length is the CAPTURE count (1), not the sequence length:
|
|
2167
|
-
// the old `i += m.length` re-processed the sequence's bracket
|
|
2168
|
-
// text as literal rows, doubling every code in a cut line
|
|
2169
|
-
// (the W21 panel-slot red test). Index 0 is the sequence.
|
|
2170
|
-
const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i))?.[0] ?? line[i];
|
|
2171
|
-
out += m;
|
|
2172
|
-
i += m.length;
|
|
2173
|
-
continue;
|
|
2174
|
-
}
|
|
2175
|
-
const cw = displayWidth(line[i]);
|
|
2176
|
-
if (width + cw > W - 1)
|
|
2177
|
-
break; // reserve the ellipsis's column
|
|
2178
|
-
out += line[i];
|
|
2179
|
-
width += cw;
|
|
2180
|
-
i += 1;
|
|
2181
|
-
}
|
|
2182
|
-
// R8a: the reset comes from the PALETTE, not hardcoded. `\x1b[0m`
|
|
2183
|
-
// here put an escape into every cut row under NO_COLOR and behind a
|
|
2184
|
-
// pipe — the one context COLOR_OFF exists to keep clean (§1.2). A
|
|
2185
|
-
// coloured palette is byte-identical, because its reset IS `\x1b[0m`.
|
|
2186
|
-
return `${out}${palette().reset}…`;
|
|
2187
|
-
}
|
|
2188
1772
|
/** W20 — the settled block's duration, the `2h 14m` form (the task
|
|
2189
1773
|
* narrative's long-horizon idiom): minutes+seconds under an hour,
|
|
2190
1774
|
* hours+minutes past it. */
|
|
@@ -2259,7 +1843,7 @@ class Checklist {
|
|
|
2259
1843
|
return [cutLine(header, W), ...itemRows.map((r) => cutLine(r, W))];
|
|
2260
1844
|
}
|
|
2261
1845
|
}
|
|
2262
|
-
// ---- the chrome components (the status container, the
|
|
1846
|
+
// ---- the chrome components (the status container, the footer) ----
|
|
2263
1847
|
/** The status container's row: the status text (+ the tail) with the
|
|
2264
1848
|
* right-aligned "/ commands · ↑ history" hint in the idle state —
|
|
2265
1849
|
* the hint CUT FIRST when the width is short (the #16g rule); when
|
|
@@ -2282,20 +1866,28 @@ class Checklist {
|
|
|
2282
1866
|
* survives longest because it is the door to everything; `ctrl+r`
|
|
2283
1867
|
* outranks `↑ history` because pressing up is how a person finds the
|
|
2284
1868
|
* history by accident, and nothing finds ctrl+r by accident. */
|
|
2285
|
-
export function idleHint(room) {
|
|
1869
|
+
export function idleHint(room, expand = null) {
|
|
2286
1870
|
// The third rung is today's hint, kept so that NO width loses
|
|
2287
1871
|
// something that used to fit: without it, a room of 24-30 columns
|
|
2288
1872
|
// fell all the way to `/ commands` even though the old form fitted.
|
|
2289
1873
|
// So the ladder is not a strict ranking of the three affordances —
|
|
2290
1874
|
// it is the widest honest form at each room, and ctrl+r is on the
|
|
2291
1875
|
// first two rungs rather than on all of them.
|
|
2292
|
-
|
|
1876
|
+
//
|
|
1877
|
+
// D-S2-1 (owner-ruled 2026-09-06): the ctrl+o SWITCH is named on the
|
|
1878
|
+
// two widest rungs, beside ctrl+r, and only while a card on screen
|
|
1879
|
+
// has something behind the key — the caller passes null otherwise,
|
|
1880
|
+
// and the ladder is then exactly the one above. It replaced the
|
|
1881
|
+
// per-card bright token (TUI2-R2 ⑤): a global switch has no single
|
|
1882
|
+
// target to mark.
|
|
1883
|
+
const switchRungs = expand === null ? [] : [` / commands · ↑ history · ctrl+o ${expand} · ctrl+r transcript`, ` / commands · ctrl+o ${expand} · ctrl+r transcript`];
|
|
1884
|
+
for (const form of [...switchRungs, " / commands · ↑ history · ctrl+r transcript", " / commands · ctrl+r transcript", " / commands · ↑ history", " / commands"]) {
|
|
2293
1885
|
if (visibleWidth(form) <= room)
|
|
2294
1886
|
return form;
|
|
2295
1887
|
}
|
|
2296
1888
|
return "";
|
|
2297
1889
|
}
|
|
2298
|
-
export function statusLine(status, tail, W, hint) {
|
|
1890
|
+
export function statusLine(status, tail, W, hint, expand = null) {
|
|
2299
1891
|
const p = palette();
|
|
2300
1892
|
const text = `${status}${tail === "" ? "" : ` · ${tail}`}`;
|
|
2301
1893
|
// W18: the hint is a parameter — the compacting row right-aligns its
|
|
@@ -2306,25 +1898,12 @@ export function statusLine(status, tail, W, hint) {
|
|
|
2306
1898
|
if (statusW > W) {
|
|
2307
1899
|
return `${p.dim}${widthCut(text, W - 1)}…${p.reset}`;
|
|
2308
1900
|
}
|
|
2309
|
-
const hintText = hint ?? idleHint(Math.max(0, W - statusW));
|
|
1901
|
+
const hintText = hint ?? idleHint(Math.max(0, W - statusW), expand);
|
|
2310
1902
|
const hintW = visibleWidth(hintText);
|
|
2311
1903
|
if (hintW === 0 || statusW + hintW > W)
|
|
2312
1904
|
return `${p.dim}${text}${p.reset}`;
|
|
2313
1905
|
return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
2314
1906
|
}
|
|
2315
|
-
/** The display-width prefix of a plain (SGR-free) text. W21: exported
|
|
2316
|
-
* for the approval panel's option-2 rule-name cut. */
|
|
2317
|
-
export function widthCut(text, max) {
|
|
2318
|
-
let w = 0;
|
|
2319
|
-
let i = 0;
|
|
2320
|
-
for (; i < text.length; i += 1) {
|
|
2321
|
-
const cw = displayWidth(text[i]);
|
|
2322
|
-
if (w + cw > max)
|
|
2323
|
-
break;
|
|
2324
|
-
w += cw;
|
|
2325
|
-
}
|
|
2326
|
-
return text.slice(0, i);
|
|
2327
|
-
}
|
|
2328
1907
|
/**
|
|
2329
1908
|
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
2330
1909
|
*
|