@vincemakes/kiso-tui-cells 0.25.0 → 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 -188
- package/dist/components.js +115 -549
- 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
|
|
@@ -880,7 +794,7 @@ class ToolExecution {
|
|
|
880
794
|
// there is no card — the call keeps its head row until it
|
|
881
795
|
// commits, which is the one form that fits anywhere.
|
|
882
796
|
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
883
|
-
const gutter =
|
|
797
|
+
const gutter = `${breathFrame(ctx.spinnerI)} `;
|
|
884
798
|
if (liveRows <= 0) {
|
|
885
799
|
// the degraded form is the head row ALONE, so it keeps the
|
|
886
800
|
// duration it would otherwise have lost with its card — cut
|
|
@@ -940,8 +854,8 @@ class ToolExecution {
|
|
|
940
854
|
* The affordance is a statement about hidden content: a cell whose body
|
|
941
855
|
* is already whole on screen must not advertise a key that would show it
|
|
942
856
|
* the same thing, and a cell that already carries its own renderer cut
|
|
943
|
-
* (
|
|
944
|
-
*
|
|
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
|
|
945
859
|
* common case — is every settled non-shell call, whose collapsed body is
|
|
946
860
|
* empty: the whole result sits behind the key with nothing on screen
|
|
947
861
|
* saying so.
|
|
@@ -1029,23 +943,13 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "
|
|
|
1029
943
|
const core = join(meta, counted, `${elapsed}s`);
|
|
1030
944
|
const withAttr = join(meta, counted, `${elapsed}s`, attr.replace(" · ", ""));
|
|
1031
945
|
const lead = `${verbCol} `;
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
return full;
|
|
1040
|
-
// 2. the attribution gives way
|
|
1041
|
-
const bare = fit(target, core);
|
|
1042
|
-
if (bare !== null)
|
|
1043
|
-
return bare;
|
|
1044
|
-
// 2b. the COUNT gives way next (pin 4), where the suffix is not
|
|
1045
|
-
// already carrying it
|
|
1046
|
-
const short = fit(target, join(meta, `${elapsed}s`));
|
|
1047
|
-
if (short !== null)
|
|
1048
|
-
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;
|
|
1049
953
|
// 3. the target truncates, the core stays whole
|
|
1050
954
|
const stem = join(meta, `${elapsed}s`);
|
|
1051
955
|
const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
|
|
@@ -1083,15 +987,12 @@ function attribution(c) {
|
|
|
1083
987
|
return "";
|
|
1084
988
|
return c.verdict.decision === "denied" ? " · denied" : " · approved";
|
|
1085
989
|
}
|
|
1086
|
-
|
|
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) {
|
|
1087
994
|
if (lines === null)
|
|
1088
995
|
return "";
|
|
1089
|
-
// R13 — the COUNT left this suffix for the head row's own `·` chain,
|
|
1090
|
-
// where the bodied card keeps it too (`… · 10 lines · 0.1s`). It was
|
|
1091
|
-
// here because the parenthesised core had nowhere to put it and the
|
|
1092
|
-
// suffix was the only tail the row had; with one grammar for both
|
|
1093
|
-
// cards there is one place, and VD-6's "stated exactly once" is what
|
|
1094
|
-
// forbids leaving a copy behind.
|
|
1095
996
|
for (const tier of [" · ctrl+o expands", " · ctrl+o"]) {
|
|
1096
997
|
if (tier.length <= room)
|
|
1097
998
|
return tier;
|
|
@@ -1106,186 +1007,43 @@ function appendSuffix(row, suffix) {
|
|
|
1106
1007
|
const p = palette();
|
|
1107
1008
|
return `${row}${p.dim}${suffix}${p.reset}`;
|
|
1108
1009
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
* row. Zero new rows, zero new columns: the affordance the cell already
|
|
1116
|
-
* prints is the marker.
|
|
1117
|
-
*
|
|
1118
|
-
* Applied to a row rather than composed into it on purpose. The token is
|
|
1119
|
-
* emitted from several places (the settled suffix, the renderer's own
|
|
1120
|
-
* `└ +N … · ctrl+o` cut rows) and threading a flag through all of them
|
|
1121
|
-
* would put the invariant "exactly one bright token" in as many hands as
|
|
1122
|
-
* there are emitters. Here it has exactly one.
|
|
1123
|
-
*
|
|
1124
|
-
* NO_COLOR: p.dim is empty, so the row's bytes are untouched.
|
|
1125
|
-
*/
|
|
1126
|
-
export function focusToken(row, W) {
|
|
1127
|
-
const p = palette();
|
|
1128
|
-
const at = row.lastIndexOf(EXPAND_KEY);
|
|
1129
|
-
if (at !== -1) {
|
|
1130
|
-
// the row already names the key — brighten the token in place, and
|
|
1131
|
-
// leave every other span exactly as it was
|
|
1132
|
-
if (p.dim === "")
|
|
1133
|
-
return row;
|
|
1134
|
-
// DC-3: the marker takes the WASH. It used to take the inline-code
|
|
1135
|
-
// tint and inherited its 1.54:1 — the cue naming the key that
|
|
1136
|
-
// reveals a cell was itself the least readable thing on a white
|
|
1137
|
-
// terminal. The wash is right for a second reason: this token has
|
|
1138
|
-
// to be UNIQUE on the frame ("exactly one bright token"), and an
|
|
1139
|
-
// attribute like bold is spent everywhere. It closes with washEnd
|
|
1140
|
-
// rather than a reset, so the surrounding dim survives instead of
|
|
1141
|
-
// having to be re-applied.
|
|
1142
|
-
return `${row.slice(0, at)}${p.lift}${EXPAND_KEY}${p.dim}${row.slice(at + EXPAND_KEY.length)}`;
|
|
1143
|
-
}
|
|
1144
|
-
// A LIVE row does not carry the affordance today, and the live cell is
|
|
1145
|
-
// the one ctrl+o takes FIRST (expandNext scans the live tail before
|
|
1146
|
-
// the committed ring) — so the row the key is aimed at was the one row
|
|
1147
|
-
// that never said the key existed. The affordance IS the marker here:
|
|
1148
|
-
// it appears on the focused row and nowhere else, which is why no
|
|
1149
|
-
// unfocused row's bytes move (every existing live-row assertion
|
|
1150
|
-
// renders a cell with no focus and is untouched).
|
|
1151
|
-
const room = W - visibleWidth(row);
|
|
1152
|
-
if (room < SUFFIX_MIN)
|
|
1153
|
-
return row; // never at the cost of invariant ①
|
|
1154
|
-
return `${row}${p.dim} · ${p.lift}${EXPAND_KEY}${p.reset}`;
|
|
1155
|
-
}
|
|
1156
|
-
/** DC-41 — the label in ONE place. The key has moved once now, and
|
|
1157
|
-
* a constant named after its binding is a comment that lies. */
|
|
1158
|
-
const EXPAND_KEY = "ctrl+o";
|
|
1159
|
-
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
1160
|
-
* rollup's expanded list carries a second clause (its members' full
|
|
1161
|
-
* 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. */
|
|
1162
1016
|
const COLLAPSE_ROW = "ctrl+o collapses";
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
* the plurals for its other-tool terms ("2 dirs", "1 match"). */
|
|
1168
|
-
export const ROLLUP_NOUN = {
|
|
1169
|
-
read_file: "files",
|
|
1170
|
-
list_dir: "dirs",
|
|
1171
|
-
search_text: "matches",
|
|
1172
|
-
};
|
|
1173
|
-
// ---- TUI2-R1 (B): the exploration rollup ----
|
|
1174
|
-
/** TUI2-R1 (B) — the exploration row's nouns. Deliberately NOT
|
|
1175
|
-
* ROLLUP_NOUN: that table says what a SINGLE-tool rollup counts
|
|
1176
|
-
* ("5 matches"), and this row counts CALLS across tools, where
|
|
1177
|
-
* "14 searches" is what happened. Both tables stay — changing the
|
|
1178
|
-
* older one would move an assertion this round did not declare. */
|
|
1179
|
-
const EXPLORE_NOUN = {
|
|
1180
|
-
read_file: ["file", "files"],
|
|
1181
|
-
list_dir: ["dir", "dirs"],
|
|
1182
|
-
search_text: ["search", "searches"],
|
|
1183
|
-
};
|
|
1184
|
-
/** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
|
|
1185
|
-
* TUI2-R2pre ④: this used to be a private three-tool table saying the
|
|
1186
|
-
* same thing as the card head's `_file` strip, in a different way and
|
|
1187
|
-
* for a different set of tools. Both are `displayVerb` now — the whole
|
|
1188
|
-
* point of the ruling is that there is ONE answer to "what does the
|
|
1189
|
-
* screen call this". The cut note, which used to be the deliberate
|
|
1190
|
-
* exception here, moved with it (see toolCutNote). */
|
|
1191
|
-
/** Whether a tool joins an exploration run. Exactly the read-only set —
|
|
1192
|
-
* writes, edits, shells and extension tools never group (a burst of
|
|
1193
|
-
* side effects is a list of things that HAPPENED, and every row of it
|
|
1194
|
-
* carries meaning). */
|
|
1195
|
-
export function isExploreTool(name) {
|
|
1196
|
-
return EXPLORE_NOUN[name] !== undefined;
|
|
1197
|
-
}
|
|
1198
|
-
/** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
|
|
1199
|
-
* scrollback, becomes ONE line — the work order's claimed shape
|
|
1200
|
-
* (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
|
|
1201
|
-
* toolStart: read_file → "reads", edit_file → "edits", the other tools
|
|
1202
|
-
* as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
|
|
1203
|
-
* in, the verb + "s" otherwise).
|
|
1204
|
-
* A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
1205
|
-
* words LEAD the one line, `✦ <chip> · thought 19s · read 5 files` —
|
|
1206
|
-
* the chip the SAME SGR-7 bracket as the live user row (#16f, side
|
|
1207
|
-
* pads included). The words take the fold's width budget and width-cut
|
|
1208
|
-
* at the end with the honest "…" (never a silent truncate — invariant
|
|
1209
|
-
* ① holds on the ONE row by construction).
|
|
1210
|
-
*
|
|
1211
|
-
* DECLARED SUPERSESSION (R3g, 2026-08-28) — A9 also ruled that "the
|
|
1212
|
-
* metadata terms give way LAST". They do not any more: the KEY does.
|
|
1213
|
-
* A9 was taken when this line carried no key, and at a width where the
|
|
1214
|
-
* chip, the full metadata and " · ctrl+o" cannot coexist, a fold with
|
|
1215
|
-
* no key is the turn's work behind a line with no way back to it. So
|
|
1216
|
-
* the order is now words, then metadata, then — never — the key. The
|
|
1217
|
-
* glyph is ✦ and the zero terms are dropped (R3b), so the example above
|
|
1218
|
-
* is written as the code renders it rather than as A9 first wrote it. */
|
|
1219
|
-
/**
|
|
1220
|
-
* R3b — what a run of work DID, in words. One definition, because two
|
|
1221
|
-
* surfaces say it: the fold line (`turnFold`, above) and the expand
|
|
1222
|
-
* header the compositor writes when that fold is opened. A second copy
|
|
1223
|
-
* would be a second answer to the same question, and the first thing to
|
|
1224
|
-
* drift would be the plurals — `search_text` is "matches", not
|
|
1225
|
-
* "searchs", and only the ROLLUP_NOUN table knows that.
|
|
1226
|
-
*
|
|
1227
|
-
* Zero terms are dropped (owner ruling, R3b): a term earns its place by
|
|
1228
|
-
* having a count.
|
|
1229
|
-
*/
|
|
1230
|
-
/**
|
|
1231
|
-
* R3g (2026-08-28) — the fold's own terms, VERB + COUNT + NOUN.
|
|
1232
|
-
*
|
|
1233
|
-
* DECLARED SUPERSESSION. R3b built these terms out of ROLLUP_NOUN,
|
|
1234
|
-
* whose own comment says not to: that table names what a single-tool
|
|
1235
|
-
* rollup COUNTS ("5 matches" — five matched lines), and this line
|
|
1236
|
-
* counts CALLS. So one search_text call rendered `1 match`, a sentence
|
|
1237
|
-
* that is false whenever the search matched any other number — which is
|
|
1238
|
-
* almost always. `shell` fell through to the verb branch and read
|
|
1239
|
-
* `4 shells`.
|
|
1240
|
-
*
|
|
1241
|
-
* The phrasing is the owner's, from the shape they asked for:
|
|
1242
|
-
* "thought 17s · read 4 files · listed 1 directory · ran 4 shell
|
|
1243
|
-
* commands". A tool with no entry says `3 × <verb>`, which counts calls
|
|
1244
|
-
* without inventing a noun for them.
|
|
1245
|
-
*/
|
|
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. */
|
|
1246
1021
|
/**
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
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.
|
|
1259
1036
|
*/
|
|
1260
1037
|
const TERM = {
|
|
1261
|
-
read_file: ["read", "
|
|
1262
|
-
edit_file: ["edited", "
|
|
1263
|
-
write_file: ["wrote", "
|
|
1264
|
-
list_dir: ["listed", "
|
|
1265
|
-
search_text: ["ran", "
|
|
1266
|
-
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"],
|
|
1267
1044
|
};
|
|
1268
|
-
const FOLD_TERM = Object.fromEntries(Object.entries(TERM).map(([name, [past, , singular, plural]]) => [name, [past, singular, plural]]));
|
|
1269
|
-
/**
|
|
1270
|
-
* R3h (fable, 2026-08-29) — WHICH TERMS COUNT OBJECTS.
|
|
1271
|
-
*
|
|
1272
|
-
* The rule, one sentence: a term that counts OBJECTS counts distinct
|
|
1273
|
-
* objects; a term that counts ACTS counts calls. `read 2 files` after
|
|
1274
|
-
* reading ONE file twice is a false sentence, and law 1.3 does not
|
|
1275
|
-
* become optional because the falsehood is small. `ran 2 searches`
|
|
1276
|
-
* after searching the same pattern twice is TRUE — the acts happened.
|
|
1277
|
-
*
|
|
1278
|
-
* The table sits beside FOLD_TERM so the two cannot drift: a tool whose
|
|
1279
|
-
* noun is a thing ("files", "directories") belongs here; a tool whose
|
|
1280
|
-
* noun is an act ("searches", "shell commands") does not.
|
|
1281
|
-
*/
|
|
1282
|
-
const FOLD_COUNTS_OBJECTS = new Set(["read_file", "edit_file", "write_file", "list_dir"]);
|
|
1283
|
-
/** Does this tool's fold term count distinct targets rather than calls? */
|
|
1284
|
-
export function foldCountsObjects(name) {
|
|
1285
|
-
return FOLD_COUNTS_OBJECTS.has(name);
|
|
1286
|
-
}
|
|
1287
1045
|
function foldTerm(name, n) {
|
|
1288
|
-
const t =
|
|
1046
|
+
const t = TERM[name];
|
|
1289
1047
|
if (t === undefined)
|
|
1290
1048
|
return `${n} × ${displayVerb(name)}`;
|
|
1291
1049
|
return `${t[0]} ${n} ${n === 1 ? t[1] : t[2]}`;
|
|
@@ -1303,41 +1061,6 @@ export function foldTerms(reads, edits, others) {
|
|
|
1303
1061
|
}
|
|
1304
1062
|
return parts;
|
|
1305
1063
|
}
|
|
1306
|
-
/**
|
|
1307
|
-
* R3i — THE STRETCH LINE: the turn's one working row, in three phases.
|
|
1308
|
-
*
|
|
1309
|
-
* A STRETCH is the run of thinking and tool calls between two blocks of
|
|
1310
|
-
* the model's prose. While it runs it is this line plus a bounded act
|
|
1311
|
-
* window; when it closes it commits as this same line, frozen, with its
|
|
1312
|
-
* key. The contract in one sentence: **the line you watch is the line
|
|
1313
|
-
* you keep** — the settle changes the mark, the tense and the key, and
|
|
1314
|
-
* nothing else.
|
|
1315
|
-
*
|
|
1316
|
-
* thinking ✧ thinking 4s
|
|
1317
|
-
* acting ✶ reading 6 files · running 4 shell commands
|
|
1318
|
-
* settled ✦ thought 9s · read 6 files · ran 4 shell commands · ctrl+o
|
|
1319
|
-
*
|
|
1320
|
-
* THE GIVE-WAY LADDER, in order, because at some width everything
|
|
1321
|
-
* cannot fit:
|
|
1322
|
-
*
|
|
1323
|
-
* 1. the human's WORDS (the A9 chip on a quiet turn) — they are on
|
|
1324
|
-
* screen above, in the chip band;
|
|
1325
|
-
* 2. the NOUNS compact, cheapest word first, and stop as soon as the
|
|
1326
|
-
* row fits — buying one cell must not spend every substitution;
|
|
1327
|
-
* 3. the COUNTS cut, with the honest "…";
|
|
1328
|
-
* 4. the TROUBLE CLAUSE cuts. The design first said it never gives
|
|
1329
|
-
* way, and that was unimplementable: a long clause overflows after
|
|
1330
|
-
* the counts have already cut to a bare "…", and invariant ①
|
|
1331
|
-
* throws on that row;
|
|
1332
|
-
* 5. the KEY gives way NEVER. A fold with no key is the turn's work
|
|
1333
|
-
* behind a line with no way back to it, which is the one thing
|
|
1334
|
-
* this row must not be.
|
|
1335
|
-
*
|
|
1336
|
-
* `…` in this file means CUT HERE and nothing else — which is why the
|
|
1337
|
-
* live phases carry no trailing ellipsis for in-flight, though the
|
|
1338
|
-
* reference implementation uses one. The moving mark and the present
|
|
1339
|
-
* tense already say it twice.
|
|
1340
|
-
*/
|
|
1341
1064
|
/**
|
|
1342
1065
|
* R3i phase 5 — THE ANSWERED QUESTION'S BLOCK.
|
|
1343
1066
|
*
|
|
@@ -1355,7 +1078,7 @@ export function foldTerms(reads, edits, others) {
|
|
|
1355
1078
|
* is emphasis, never information). A typed answer says `(typed)`,
|
|
1356
1079
|
* because where an answer came from is a fact about it.
|
|
1357
1080
|
*
|
|
1358
|
-
* It is WORDS, not work (law 1.7):
|
|
1081
|
+
* It is WORDS, not work (law 1.7): no summary ever stands for it,
|
|
1359
1082
|
* because the one thing a summary must not do is speak for the human.
|
|
1360
1083
|
*
|
|
1361
1084
|
* A result that is not the ask's own JSON yields NOTHING. This renderer
|
|
@@ -1393,45 +1116,11 @@ export function askedBlock(resultText, seconds, W) {
|
|
|
1393
1116
|
}),
|
|
1394
1117
|
];
|
|
1395
1118
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
];
|
|
1402
|
-
/** R3i — a stretch of exactly ONE call names its TARGET instead of its
|
|
1403
|
-
* count. `thought 2s · read 1 file` replaces two rows — the thinking
|
|
1404
|
-
* and the call — with a row that says less than either of them did,
|
|
1405
|
-
* and "thinking plus one call" is the commonest shape a narrating
|
|
1406
|
-
* model makes. This is the answer to the defect R3d killed R3b's
|
|
1407
|
-
* per-segment folds over; the "absorbs at least two rows" rule alone
|
|
1408
|
-
* does not answer it. */
|
|
1409
|
-
function stretchTerms(t, live) {
|
|
1410
|
-
// R4 — the tense is per TERM. A name with nothing in flight is in the
|
|
1411
|
-
// past whatever the line's own phase is; with liveNames absent (the
|
|
1412
|
-
// settled line) every term follows the line.
|
|
1413
|
-
const tense = (name) => (live && (t.liveNames === undefined || t.liveNames.includes(name)) ? 1 : 0);
|
|
1414
|
-
const total = t.calls.reduce((n, [, c]) => n + c, 0);
|
|
1415
|
-
// R4 — the one-call TARGET form is the SETTLED line's. Live, the act
|
|
1416
|
-
// slot directly below already names the target on its head row, so
|
|
1417
|
-
// the line was printing the same words twice, one above the other
|
|
1418
|
-
// (`running npm run check` over `shell npm run check`). Settled there
|
|
1419
|
-
// is no slot, and naming the target is strictly more than counting to
|
|
1420
|
-
// one — which is the R3i rule this keeps, where it applies.
|
|
1421
|
-
if (total === 1 && t.targets.length === 1 && !live) {
|
|
1422
|
-
const [name] = t.calls[0];
|
|
1423
|
-
const e = TERM[name];
|
|
1424
|
-
return [`${e === undefined ? name : e[0]} ${t.targets[0]}`];
|
|
1425
|
-
}
|
|
1426
|
-
return t.calls
|
|
1427
|
-
.filter(([, n]) => n > 0)
|
|
1428
|
-
.map(([name, n]) => {
|
|
1429
|
-
const e = TERM[name];
|
|
1430
|
-
if (e === undefined)
|
|
1431
|
-
return `${n} × ${displayVerb(name)}`;
|
|
1432
|
-
return `${e[tense(name)]} ${n} ${n === 1 ? e[2] : e[3]}`;
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
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. */
|
|
1435
1124
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1436
1125
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
1437
1126
|
* (the W7 table). The renderer-cut row is inside the cap. */
|
|
@@ -1441,14 +1130,7 @@ export const CAP_PREVIEW = 5;
|
|
|
1441
1130
|
/** DC-46 — the running window's ceiling is the SETTLED preview's, and a
|
|
1442
1131
|
* running card reaches it by growing rather than by being handed it.
|
|
1443
1132
|
* `LIVE_WINDOW` (CAP_PREVIEW + 1) retires with the allocation it sized. */
|
|
1444
|
-
/** The rows a card costs besides its window: two pads, the head, two
|
|
1445
|
-
* blanks and the status row. Below this there is no card (DC-43). */
|
|
1446
|
-
export const CARD_CHROME = 6;
|
|
1447
1133
|
const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
1448
|
-
/** The block body rows' prefixes (W2's gutter table): │ a bounded
|
|
1449
|
-
* block's body, └ the block's last row — what was cut, where the rest
|
|
1450
|
-
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
1451
|
-
* distinguishes the states at --plain). Structural (constraint 1). */
|
|
1452
1134
|
/** R8a — A TOOL BLOCK'S ROWS ARE INDENTED, NOT GUTTERED.
|
|
1453
1135
|
*
|
|
1454
1136
|
* `│ ` on every row drew a bar down the left of every multi-row
|
|
@@ -1462,8 +1144,7 @@ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
|
1462
1144
|
* row (see openBlock). In-block notes take the same indent,
|
|
1463
1145
|
* no glyph — because a second `└` inside one block would be the same
|
|
1464
1146
|
* mark meaning two things (§4.1). CUT_ROW is unchanged for the
|
|
1465
|
-
* surfaces that are not a tool block
|
|
1466
|
-
* slot's overflow count. */
|
|
1147
|
+
* surfaces that are not a tool block. */
|
|
1467
1148
|
/** R8a's four columns — off the surface. Inside a painted card every
|
|
1468
1149
|
* row sits at column 2 (R13 E4): the head row and the outcome row
|
|
1469
1150
|
* bracket the preview, so the indent is no longer what says "these
|
|
@@ -1533,10 +1214,18 @@ function slabRow(inner, W) {
|
|
|
1533
1214
|
* metadata rows: the parts give way in a PINNED ORDER, and the part
|
|
1534
1215
|
* that carries the semantics is the one reserved. */
|
|
1535
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) {
|
|
1536
1225
|
for (const t of tiers)
|
|
1537
1226
|
if (visibleWidth(t) <= room)
|
|
1538
1227
|
return t;
|
|
1539
|
-
return
|
|
1228
|
+
return null;
|
|
1540
1229
|
}
|
|
1541
1230
|
function noteRow(text, W, tone) {
|
|
1542
1231
|
const p = palette();
|
|
@@ -1874,91 +1563,12 @@ function liveCap(c, room) {
|
|
|
1874
1563
|
liveHighWater.set(c, cap);
|
|
1875
1564
|
return cap;
|
|
1876
1565
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
*
|
|
1884
|
-
* R3i built the same window INTERMITTENTLY — a running call got its
|
|
1885
|
-
* fixed 1+3 block (W8), a finished one got nothing — so the live
|
|
1886
|
-
* region's height was a function of how many calls happened to be in
|
|
1887
|
-
* flight this frame. Over one real stretch that is 2 rows, then 7,
|
|
1888
|
-
* then 2, then 17 for a three-call batch, then 2 again, and every
|
|
1889
|
-
* transition scrolls everything above it. The owner's report was that
|
|
1890
|
-
* the screen "keeps jumping", and it was an accurate description of
|
|
1891
|
-
* the design, not a defect in its execution.
|
|
1892
|
-
*
|
|
1893
|
-
* The cure is not a smaller window, it is a STANDING one: between two
|
|
1894
|
-
* calls the slot keeps the call that just finished rather than
|
|
1895
|
-
* collapsing, and before any call it keeps the thinking that is
|
|
1896
|
-
* producing them — which is R3i ruling 5 ("thinking belongs on the
|
|
1897
|
-
* stretch line, IN THE ACT WINDOW, and in full in expansions") finally
|
|
1898
|
-
* wired, since R3i stated it while building no window for the thinking
|
|
1899
|
-
* phase to live in.
|
|
1900
|
-
*
|
|
1901
|
-
* Four rows, deliberately the same 1+3 shape W8 gave a running call, so
|
|
1902
|
-
* the commonest frame — exactly one call in flight — renders byte-for-
|
|
1903
|
-
* byte what 0.17.0 shipped.
|
|
1904
|
-
*/
|
|
1905
|
-
export const ACT_SLOT_ROWS = 4;
|
|
1906
|
-
/**
|
|
1907
|
-
* R4 — the slot's body rows: the tail of `text`, newest at the BOTTOM,
|
|
1908
|
-
* bottom-padded to exactly `rows`.
|
|
1909
|
-
*
|
|
1910
|
-
* The same dim │ gutter a running call's window uses (W2's table), and
|
|
1911
|
-
* the same two VD-4 rules: leading blank gutters are skipped, and the
|
|
1912
|
-
* short-output pad goes at the BOTTOM so output starts under its own
|
|
1913
|
-
* header and grows downward. The slot's CONTENTS change; its shape
|
|
1914
|
-
* does not.
|
|
1915
|
-
*/
|
|
1916
|
-
export function slotTail(text, W, rows) {
|
|
1917
|
-
if (rows <= 0)
|
|
1918
|
-
return [];
|
|
1919
|
-
const p = palette();
|
|
1920
|
-
const all = blockRows(text, W);
|
|
1921
|
-
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1922
|
-
const body = from < 0 ? [] : all.slice(from);
|
|
1923
|
-
// R7a: no pad. The slot stopped padding (see slotPad) and this was
|
|
1924
|
-
// the same pad by another route — three blank rows under a call with
|
|
1925
|
-
// nothing to say yet, which is the hole a7's blank-run guard prices.
|
|
1926
|
-
// R8a: the corner opens whatever slice survives the cap.
|
|
1927
|
-
return openBlock(body.slice(Math.max(0, body.length - rows)));
|
|
1928
|
-
}
|
|
1929
|
-
/** R4 — clamp or pad assembled slot rows to EXACTLY `rows`. The padding
|
|
1930
|
-
* is what makes the slot stand; the clamp is what keeps the slot from
|
|
1931
|
-
* ever being the thing that trips the force-commit cap (a slot that
|
|
1932
|
-
* could overflow would commit real cells to relieve blank rows). */
|
|
1933
|
-
export function slotPad(content, rows) {
|
|
1934
|
-
if (rows <= 0)
|
|
1935
|
-
return [];
|
|
1936
|
-
// R7a — THE SLOT NO LONGER PADS. It caps, and that is all.
|
|
1937
|
-
//
|
|
1938
|
-
// R4 padded to a fixed height because the slot's content came and
|
|
1939
|
-
// went: a finished call left the block, the block shrank, and every
|
|
1940
|
-
// row above it moved. The pad bought stability with rows drawn as
|
|
1941
|
-
// `│`, which is why a tall empty gutter ran down the screen under
|
|
1942
|
-
// every short block — the owner's own screenshot, and law 1.3's
|
|
1943
|
-
// case: a mark on a row with nothing to mark.
|
|
1944
|
-
//
|
|
1945
|
-
// Blanking the gutter revealed the hole it had been covering, and
|
|
1946
|
-
// the a7 replay priced the hole: blank runs over 2 in 653 of 733
|
|
1947
|
-
// frames, the screen never durably filling. So the pad had to go —
|
|
1948
|
-
// and the height it was buying is now bought by the CONTENT, since
|
|
1949
|
-
// R7a keeps every call's row for the life of the stretch. A block
|
|
1950
|
-
// whose rows only accumulate cannot shrink, so there is nothing
|
|
1951
|
-
// left for a pad to hold up. Measured: 65 of 733 at 40x24, the
|
|
1952
|
-
// pre-R7a number exactly, with the motion gates still green.
|
|
1953
|
-
return content.slice(0, rows);
|
|
1954
|
-
}
|
|
1955
|
-
/** R4 — the slot's overflow row: the calls in flight beyond the head
|
|
1956
|
-
* budget. It lives INSIDE the slot (it is one of the four rows), which
|
|
1957
|
-
* is what keeps a parallel burst from growing the region. */
|
|
1958
|
-
export function moreRunningRow(n, W) {
|
|
1959
|
-
const p = palette();
|
|
1960
|
-
return cutLine(` ${p.dim}${CUT_ROW}+${n} more running${p.reset}`, W);
|
|
1961
|
-
}
|
|
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. */
|
|
1962
1572
|
/** W12: the delegate's child sessions collapse to the tool row plus ONE
|
|
1963
1573
|
* line — the height NEVER changes (running → settled replaces the row
|
|
1964
1574
|
* in place). The running row derives from the INPUT: the parent has no
|
|
@@ -2159,45 +1769,6 @@ class Banner {
|
|
|
2159
1769
|
* done-collapse. Every live row CUTS at W (never folds) — the block's
|
|
2160
1770
|
* height is its row count. */
|
|
2161
1771
|
export const CAP_TASK_LIVE = 6;
|
|
2162
|
-
/** W20 — the live block's fixed-window row cut: an SGR-aware ONE-ROW
|
|
2163
|
-
* truncation (foldLine wraps; a wrapped row would break the height
|
|
2164
|
-
* cap — every live row is exactly one screen row at every width).
|
|
2165
|
-
* A line that fits (≤ W) passes through whole; an overflow cuts the
|
|
2166
|
-
* content at W−1 — the ellipsis's slot — and the ellipsis rides AFTER
|
|
2167
|
-
* the reset (post-reset — the PTY needles' convention). The cut row
|
|
2168
|
-
* never exceeds W (invariant ①). W21: exported for the approval
|
|
2169
|
-
* panel's single-row lines (the rule line, the title, the divider,
|
|
2170
|
-
* the options/affordance rows). */
|
|
2171
|
-
export function cutLine(line, W) {
|
|
2172
|
-
if (visibleWidth(line) <= W)
|
|
2173
|
-
return line;
|
|
2174
|
-
let out = "";
|
|
2175
|
-
let width = 0;
|
|
2176
|
-
for (let i = 0; i < line.length;) {
|
|
2177
|
-
if (line[i] === "\x1b") {
|
|
2178
|
-
// exec returns an ARRAY — copying m coerces it (the match), but
|
|
2179
|
-
// m.length is the CAPTURE count (1), not the sequence length:
|
|
2180
|
-
// the old `i += m.length` re-processed the sequence's bracket
|
|
2181
|
-
// text as literal rows, doubling every code in a cut line
|
|
2182
|
-
// (the W21 panel-slot red test). Index 0 is the sequence.
|
|
2183
|
-
const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i))?.[0] ?? line[i];
|
|
2184
|
-
out += m;
|
|
2185
|
-
i += m.length;
|
|
2186
|
-
continue;
|
|
2187
|
-
}
|
|
2188
|
-
const cw = displayWidth(line[i]);
|
|
2189
|
-
if (width + cw > W - 1)
|
|
2190
|
-
break; // reserve the ellipsis's column
|
|
2191
|
-
out += line[i];
|
|
2192
|
-
width += cw;
|
|
2193
|
-
i += 1;
|
|
2194
|
-
}
|
|
2195
|
-
// R8a: the reset comes from the PALETTE, not hardcoded. `\x1b[0m`
|
|
2196
|
-
// here put an escape into every cut row under NO_COLOR and behind a
|
|
2197
|
-
// pipe — the one context COLOR_OFF exists to keep clean (§1.2). A
|
|
2198
|
-
// coloured palette is byte-identical, because its reset IS `\x1b[0m`.
|
|
2199
|
-
return `${out}${palette().reset}…`;
|
|
2200
|
-
}
|
|
2201
1772
|
/** W20 — the settled block's duration, the `2h 14m` form (the task
|
|
2202
1773
|
* narrative's long-horizon idiom): minutes+seconds under an hour,
|
|
2203
1774
|
* hours+minutes past it. */
|
|
@@ -2272,7 +1843,7 @@ class Checklist {
|
|
|
2272
1843
|
return [cutLine(header, W), ...itemRows.map((r) => cutLine(r, W))];
|
|
2273
1844
|
}
|
|
2274
1845
|
}
|
|
2275
|
-
// ---- the chrome components (the status container, the
|
|
1846
|
+
// ---- the chrome components (the status container, the footer) ----
|
|
2276
1847
|
/** The status container's row: the status text (+ the tail) with the
|
|
2277
1848
|
* right-aligned "/ commands · ↑ history" hint in the idle state —
|
|
2278
1849
|
* the hint CUT FIRST when the width is short (the #16g rule); when
|
|
@@ -2295,20 +1866,28 @@ class Checklist {
|
|
|
2295
1866
|
* survives longest because it is the door to everything; `ctrl+r`
|
|
2296
1867
|
* outranks `↑ history` because pressing up is how a person finds the
|
|
2297
1868
|
* history by accident, and nothing finds ctrl+r by accident. */
|
|
2298
|
-
export function idleHint(room) {
|
|
1869
|
+
export function idleHint(room, expand = null) {
|
|
2299
1870
|
// The third rung is today's hint, kept so that NO width loses
|
|
2300
1871
|
// something that used to fit: without it, a room of 24-30 columns
|
|
2301
1872
|
// fell all the way to `/ commands` even though the old form fitted.
|
|
2302
1873
|
// So the ladder is not a strict ranking of the three affordances —
|
|
2303
1874
|
// it is the widest honest form at each room, and ctrl+r is on the
|
|
2304
1875
|
// first two rungs rather than on all of them.
|
|
2305
|
-
|
|
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"]) {
|
|
2306
1885
|
if (visibleWidth(form) <= room)
|
|
2307
1886
|
return form;
|
|
2308
1887
|
}
|
|
2309
1888
|
return "";
|
|
2310
1889
|
}
|
|
2311
|
-
export function statusLine(status, tail, W, hint) {
|
|
1890
|
+
export function statusLine(status, tail, W, hint, expand = null) {
|
|
2312
1891
|
const p = palette();
|
|
2313
1892
|
const text = `${status}${tail === "" ? "" : ` · ${tail}`}`;
|
|
2314
1893
|
// W18: the hint is a parameter — the compacting row right-aligns its
|
|
@@ -2319,25 +1898,12 @@ export function statusLine(status, tail, W, hint) {
|
|
|
2319
1898
|
if (statusW > W) {
|
|
2320
1899
|
return `${p.dim}${widthCut(text, W - 1)}…${p.reset}`;
|
|
2321
1900
|
}
|
|
2322
|
-
const hintText = hint ?? idleHint(Math.max(0, W - statusW));
|
|
1901
|
+
const hintText = hint ?? idleHint(Math.max(0, W - statusW), expand);
|
|
2323
1902
|
const hintW = visibleWidth(hintText);
|
|
2324
1903
|
if (hintW === 0 || statusW + hintW > W)
|
|
2325
1904
|
return `${p.dim}${text}${p.reset}`;
|
|
2326
1905
|
return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
2327
1906
|
}
|
|
2328
|
-
/** The display-width prefix of a plain (SGR-free) text. W21: exported
|
|
2329
|
-
* for the approval panel's option-2 rule-name cut. */
|
|
2330
|
-
export function widthCut(text, max) {
|
|
2331
|
-
let w = 0;
|
|
2332
|
-
let i = 0;
|
|
2333
|
-
for (; i < text.length; i += 1) {
|
|
2334
|
-
const cw = displayWidth(text[i]);
|
|
2335
|
-
if (w + cw > max)
|
|
2336
|
-
break;
|
|
2337
|
-
w += cw;
|
|
2338
|
-
}
|
|
2339
|
-
return text.slice(0, i);
|
|
2340
|
-
}
|
|
2341
1907
|
/**
|
|
2342
1908
|
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
2343
1909
|
*
|