@vincemakes/kiso-tui-cells 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +27 -68
- package/dist/components.js +430 -518
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.js +17 -0
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* lives HERE: every line a component returns must fit the terminal
|
|
9
9
|
* width — the compositor's crash-on-violation invariant backs it up
|
|
10
10
|
* (a component that forgets to fold CRASHES with a diagnostic, never
|
|
11
|
-
* silently truncates — the crash is the contract
|
|
11
|
+
* silently truncates — the crash is the contract UNDER TEST; in the
|
|
12
|
+
* field the row is cut and the fact is said, once, through the notice
|
|
13
|
+
* channel. DECLARED REVERSAL of "the crash is the contract, not a
|
|
14
|
+
* symptom" (owner-lane, 2026-09-04): two instances of this class in two
|
|
15
|
+
* days, one caught by a gate (DC-45) and one by the owner on the first
|
|
16
|
+
* frame of an ordinary command (DC-48). In a gate the crash is right; in
|
|
17
|
+
* a human's hands it costs them the composer and the session to save
|
|
18
|
+
* them a row one column too wide. `KISO_INVARIANTS=throw` is what every
|
|
19
|
+
* suite here runs under).
|
|
12
20
|
*
|
|
13
21
|
* The fold is SGR-AWARE: a line whose bold/dim span would straddle a
|
|
14
22
|
* fold boundary closes the span at the break and reopens it on the
|
|
@@ -118,9 +126,20 @@ export { visibleWidth } from "./width.js";
|
|
|
118
126
|
export function bodySpacing(prev, rows) {
|
|
119
127
|
if (rows.length === 0 || prev === null || prev.length === 0)
|
|
120
128
|
return rows;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
// R13 D1 — ONE blank between any two elements, whatever their height.
|
|
130
|
+
//
|
|
131
|
+
// W11 spaced by height: one-row siblings packed tight, anything
|
|
132
|
+
// multi-row breathed on both sides. A reader could not tell where the
|
|
133
|
+
// next blank would fall — and worse, the spacing was a function of a
|
|
134
|
+
// cell's CURRENT height, so a cell growing from one row to five moved
|
|
135
|
+
// everything around it. That is the mechanism behind R7a and behind
|
|
136
|
+
// R12 Round 2's settle shift, both of them "the screen moved under
|
|
137
|
+
// the reader".
|
|
138
|
+
//
|
|
139
|
+
// A constant cannot do that: a live block, its settled form and the
|
|
140
|
+
// card it becomes are spaced identically BY CONSTRUCTION, which is
|
|
141
|
+
// exactly what R7a's one-row stand-in was simulating.
|
|
142
|
+
return ["", ...rows];
|
|
124
143
|
}
|
|
125
144
|
/** The container — vertical concatenation with the W11 formula. No
|
|
126
145
|
* component decides its own spacing: every blank in the body is the
|
|
@@ -212,6 +231,9 @@ export function cellComponent(cell) {
|
|
|
212
231
|
* the same rule as three thousand short ones.
|
|
213
232
|
*/
|
|
214
233
|
const USER_CHIP_ROWS = 12;
|
|
234
|
+
/** R13 D4 — the chip's inner pad: two columns, so its text begins in
|
|
235
|
+
* the same column as everything else on the page. */
|
|
236
|
+
const CHIP_PAD = " ";
|
|
215
237
|
class UserMessage {
|
|
216
238
|
cell;
|
|
217
239
|
constructor(cell) {
|
|
@@ -219,7 +241,12 @@ class UserMessage {
|
|
|
219
241
|
}
|
|
220
242
|
render(W, _ctx) {
|
|
221
243
|
const p = palette();
|
|
222
|
-
|
|
244
|
+
// R13 D4 — TWO columns of inner pad, where R2 had one. The chip
|
|
245
|
+
// keeps its surface (reverse video, one row per folded line, no
|
|
246
|
+
// pad rows — R12 Round 2's ruling stands); what changes is where
|
|
247
|
+
// its text STARTS, so the human's words begin in the same column
|
|
248
|
+
// as the model's (E3) and as a card's rows (E4).
|
|
249
|
+
const chipW = Math.max(1, W - 2 * CHIP_PAD.length);
|
|
223
250
|
const rows = [];
|
|
224
251
|
// REL-0152-D13: fold only as far as the bound needs. A pasted file
|
|
225
252
|
// has thousands of lines and folding all of them to show twelve is
|
|
@@ -264,7 +291,7 @@ class UserMessage {
|
|
|
264
291
|
// row is two cells per character and pads by cells.
|
|
265
292
|
const inner = chipW;
|
|
266
293
|
for (const row of content) {
|
|
267
|
-
rows.push(`${p.rv}
|
|
294
|
+
rows.push(`${p.rv}${CHIP_PAD}${row}${" ".repeat(Math.max(0, inner - displayWidth(row)))}${CHIP_PAD}${p.rvEnd}`);
|
|
268
295
|
}
|
|
269
296
|
if (!truncated)
|
|
270
297
|
return rows;
|
|
@@ -276,7 +303,16 @@ class UserMessage {
|
|
|
276
303
|
const shown = rows.length;
|
|
277
304
|
const total = paras.length;
|
|
278
305
|
const more = Math.max(0, total - shown);
|
|
279
|
-
|
|
306
|
+
// DC-45: THE NOTICE FOLDS TOO. It was written at a fixed 30 columns
|
|
307
|
+
// and emitted verbatim at every width, so a paste of thirteen lines
|
|
308
|
+
// in a terminal narrower than the sentence tripped the compositor's
|
|
309
|
+
// invariant ① and killed the session. The tiers are TUI2-R1.5 ⑤'s
|
|
310
|
+
// discipline: `sent in full` is the SEMANTICS — the whole reason
|
|
311
|
+
// the row exists — so the count gives way before it, and `cutLine`
|
|
312
|
+
// is the backstop that holds at any width there is.
|
|
313
|
+
const count = more > 0 ? `+${more} more line${more === 1 ? "" : "s"}` : "cut here";
|
|
314
|
+
const short = more > 0 ? `+${more}` : "cut";
|
|
315
|
+
rows.push(cutLine(`${p.dim}\u2514 ${pickTier([`${count} \u00b7 sent in full`, `${short} \u00b7 sent in full`, "sent in full", count], Math.max(1, W - 2))}${p.reset}`, W));
|
|
280
316
|
return rows;
|
|
281
317
|
}
|
|
282
318
|
}
|
|
@@ -338,7 +374,27 @@ class ThinkingBlock {
|
|
|
338
374
|
const text = escapeTerminal(this.cell.text).trim();
|
|
339
375
|
if (text === "")
|
|
340
376
|
return [];
|
|
341
|
-
|
|
377
|
+
// DC-47 — THINKING GOES ONE LEVEL DEEPER THAN PROSE, and the
|
|
378
|
+
// reason is a law rather than a taste.
|
|
379
|
+
//
|
|
380
|
+
// §7.2: the indent is the price of §1.2 — italic and dim are
|
|
381
|
+
// escape sequences, so a rendered frame with its colour stripped
|
|
382
|
+
// (a terminal capture, a paste out of the scrollback, a log of
|
|
383
|
+
// what was drawn) would lose the line between the model's
|
|
384
|
+
// reasoning and its answer. R13's E3 moved PROSE to column 2,
|
|
385
|
+
// which is where the thinking already was, so after
|
|
386
|
+
// `sed 's/\x1b\[[0-9;]*m//g'` the two became the same row.
|
|
387
|
+
// Measured, not reasoned: both rendered
|
|
388
|
+
// `" Weighing the two shapes."` exactly.
|
|
389
|
+
//
|
|
390
|
+
// NOT a pipe, though §7.2 used to say so: `thinkingEnd`'s inactive
|
|
391
|
+
// path writes `foldThinking` — one dim line — so a pipe never sees
|
|
392
|
+
// a thinking paragraph to confuse with prose.
|
|
393
|
+
//
|
|
394
|
+
// So the thinking takes the next column in. It is still the only
|
|
395
|
+
// carrier that survives a pipe, and it is still one indent step —
|
|
396
|
+
// what moved is which step, because prose took the one it had.
|
|
397
|
+
const room = Math.max(1, W - THINK_COL.length);
|
|
342
398
|
const rows = [];
|
|
343
399
|
for (const para of text.split(/\n\s*\n/)) {
|
|
344
400
|
const flat = para.replace(/\s+/g, " ").trim();
|
|
@@ -350,7 +406,7 @@ class ThinkingBlock {
|
|
|
350
406
|
// invariant ①b (a row is one physical row) holds by
|
|
351
407
|
// construction rather than by remembering to split.
|
|
352
408
|
for (const line of foldLine(flat, room))
|
|
353
|
-
rows.push(
|
|
409
|
+
rows.push(`${THINK_COL}${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
|
|
354
410
|
}
|
|
355
411
|
return rows;
|
|
356
412
|
}
|
|
@@ -633,39 +689,10 @@ class ToolExecution {
|
|
|
633
689
|
const c = this.cell;
|
|
634
690
|
const verb = escapeTerminal(displayVerb(c.name));
|
|
635
691
|
const verbCol = verb.length < 5 ? `${verb}${" ".repeat(5 - verb.length)}` : verb;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
// being told), the timing and the affordance dim — the
|
|
641
|
-
// prototype's placement. The affordance names what the key
|
|
642
|
-
// SHOWS here ("lists them"), because a group row's expand is a
|
|
643
|
-
// list of calls, not a body of output.
|
|
644
|
-
const r = c.rolled;
|
|
645
|
-
const counts = exploreCounts(parts);
|
|
646
|
-
const head = ` explored ${p.bold}${counts}${p.reset}`; // R2: no tick
|
|
647
|
-
const tail = ` (${r.elapsed}s)`;
|
|
648
|
-
const room = W - visibleWidth(head) - tail.length;
|
|
649
|
-
const affordance = " · ctrl+o lists them";
|
|
650
|
-
return [cutLine(`${head}${p.dim}${tail}${affordance.length <= room ? affordance : ""}${p.reset}`, W)];
|
|
651
|
-
}
|
|
652
|
-
if (c.rolled !== null) {
|
|
653
|
-
// W13 — the rolled-up group's ONE row + the target children:
|
|
654
|
-
// the work order's claimed shape, verbatim — the verbCol's
|
|
655
|
-
// 5-char pad reproduces the "read 5 files" double space, the
|
|
656
|
-
// children are the first 3 basename targets, the overflow row
|
|
657
|
-
// carries the ctrl+o affordance (its "└ … ctrl+o" joins the
|
|
658
|
-
// W15 expand history — the head's commit captures it).
|
|
659
|
-
const r = c.rolled;
|
|
660
|
-
const noun = ROLLUP_NOUN[c.name] ?? "calls";
|
|
661
|
-
const out = gutterCut(" ", `${verbCol} ${r.count} ${noun} (${kUnit(r.lines)} lines, ${r.elapsed}s)`, W); // R2: no tick
|
|
662
|
-
const shown = r.targets.slice(0, 3);
|
|
663
|
-
if (shown.length > 0)
|
|
664
|
-
out.push(` ${p.dim}${CUT_ROW}${escapeTerminal(shown.join(" · "))}${p.reset}`);
|
|
665
|
-
if (r.targets.length > 3)
|
|
666
|
-
out.push(` ${p.dim}${CUT_ROW}+${r.targets.length - 3} more — ctrl+o expands${p.reset}`);
|
|
667
|
-
return out;
|
|
668
|
-
}
|
|
692
|
+
// R13 — the W13 rollup row and TUI2-R1 (B)'s exploration row stood
|
|
693
|
+
// here, ahead of everything else a settled call could be. Both are
|
|
694
|
+
// retired with the `rolled` field they read (see the compositor's
|
|
695
|
+
// #foldOrRollup for the reversal in full).
|
|
669
696
|
if (c.state === "done") {
|
|
670
697
|
// R3i phase 5: an answered (or declined) ask_user renders its
|
|
671
698
|
// OWN block — the questions and what the human said. The row
|
|
@@ -689,7 +716,7 @@ class ToolExecution {
|
|
|
689
716
|
if (c.reason !== null) {
|
|
690
717
|
const by = attribution(c);
|
|
691
718
|
const out = gutterCut(" ", `${p.red}${escapeTerminal(`${c.name} ${toolTargetOf(c)}`)} (${escapeTerminal(c.reason)}${by})${p.reset}`, W);
|
|
692
|
-
out.push(...toolBlockBody(c, W));
|
|
719
|
+
out.push(...toolBlockBody(c, W, ctx));
|
|
693
720
|
return out;
|
|
694
721
|
}
|
|
695
722
|
const elapsed = c.startedAt !== null && c.doneAt !== null ? ((c.doneAt - c.startedAt) / 1000).toFixed(1) : "?";
|
|
@@ -703,8 +730,17 @@ class ToolExecution {
|
|
|
703
730
|
// "200 of 250 lines", a diff's "+1 -1", a shell's "exit 0" — is a
|
|
704
731
|
// different fact and stays.
|
|
705
732
|
const rawMeta = settledMeta(c);
|
|
706
|
-
|
|
707
|
-
|
|
733
|
+
// VD-6's suppression RETIRED with the thing it was suppressing.
|
|
734
|
+
// A read card said `(2 lines, 0.0s) · 2 lines · ctrl+o expands`
|
|
735
|
+
// because the parens and the suffix were written by different
|
|
736
|
+
// rounds, each unaware the other was counting; the fix blanked
|
|
737
|
+
// the meta whenever the suffix would repeat it. R13 takes the
|
|
738
|
+
// count OUT of the suffix, so there is one place again and the
|
|
739
|
+
// meta is it — blanking it now would drop the fact entirely.
|
|
740
|
+
// "Stated exactly once" is unchanged and is what `countedHead`
|
|
741
|
+
// below preserves: a meta that already counts lines gets no
|
|
742
|
+
// second count beside it.
|
|
743
|
+
const meta = escapeTerminal(rawMeta);
|
|
708
744
|
// A4: the target rides the settled head row — the verb's
|
|
709
745
|
// summary column (W3's 5-char pad keeps the paths lined up).
|
|
710
746
|
// A5: an extension's auto-approval appends `· approved by
|
|
@@ -724,7 +760,9 @@ class ToolExecution {
|
|
|
724
760
|
// TUI2-R1.5 ⑤: the shortest tier is RESERVED — the affordance is
|
|
725
761
|
// the semantics. TUI2-R1.5 pin 4: and the parts give way in a
|
|
726
762
|
// PINNED ORDER, rather than whichever happened to be last.
|
|
727
|
-
const
|
|
763
|
+
const nAll = countLines(c.resultText);
|
|
764
|
+
const countedHead = nAll > 0 && !/^\d+( of \d+)? lines?$/.test(rawMeta) ? `${nAll} line${nAll === 1 ? "" : "s"}` : "";
|
|
765
|
+
const text = settledHeadText(verbCol, escapeTerminal(toolTargetOf(c)), meta, approvedBy, elapsed, W - 2 - (hidden === null ? 0 : SUFFIX_MIN), countedHead);
|
|
728
766
|
// R2 (owner, 2026-08-27): no tick, no cross. A symbol earns its
|
|
729
767
|
// cell by carrying a fact the words do not, and a row that
|
|
730
768
|
// already says `exit 0` does not need one more thing saying it
|
|
@@ -732,8 +770,7 @@ class ToolExecution {
|
|
|
732
770
|
// metadata, in words, which is also the only form that survives
|
|
733
771
|
// a pipe with the colour stripped. A failure keeps its colour
|
|
734
772
|
// AND its words — see settledMeta.
|
|
735
|
-
const
|
|
736
|
-
const body = parts.rows;
|
|
773
|
+
const body = toolBlockParts(c, W, ctx).rows;
|
|
737
774
|
if (body.length > 0 && c.expanded) {
|
|
738
775
|
// An EXPANDED block is already showing everything, and its own
|
|
739
776
|
// footer ("ctrl+o collapses") is what closes it. Giving it an
|
|
@@ -743,13 +780,13 @@ class ToolExecution {
|
|
|
743
780
|
const head = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
744
781
|
return slabBlock(appendSuffix(head, expandSuffix(hidden, W - visibleWidth(head))), body, null, W);
|
|
745
782
|
}
|
|
746
|
-
if (
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
783
|
+
if (body.length > 0) {
|
|
784
|
+
// R13 — THE CARD, when there is something to preview: the head
|
|
785
|
+
// row names the call, the preview sits inside, and the outcome
|
|
786
|
+
// CLOSES it on its own line (§7.5's words, moved off the head
|
|
787
|
+
// row because the head row is no longer the only row). D6: the
|
|
788
|
+
// target is bold there — the head row's job is to say WHAT was
|
|
789
|
+
// run, and the metadata has its own row now.
|
|
753
790
|
//
|
|
754
791
|
// A failure takes NO tint on the head row (R9): only the
|
|
755
792
|
// outcome word is coloured, which is §1.2 exactly — the
|
|
@@ -767,28 +804,28 @@ class ToolExecution {
|
|
|
767
804
|
// what happened and how long it took — is never cut open.
|
|
768
805
|
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
769
806
|
const attr = approvedBy.replace(/^ · /, "");
|
|
770
|
-
const words = pickTier([join(meta, counted, `${elapsed}s`, attr), join(meta, counted, `${elapsed}s`), join(meta, `${elapsed}s`), meta], W - visibleWidth(
|
|
807
|
+
const words = pickTier([join(meta, counted, `${elapsed}s`, attr), join(meta, counted, `${elapsed}s`), join(meta, `${elapsed}s`), meta], W - visibleWidth(noteIndent()));
|
|
771
808
|
const outcome = c.isError ? `${p.red}${words}${p.reset}` : words;
|
|
772
809
|
return slabBlock(head, body, outcome, W);
|
|
773
810
|
}
|
|
774
|
-
//
|
|
775
|
-
//
|
|
811
|
+
// R13 — nothing to preview: the SAME card, three rows, with the
|
|
812
|
+
// outcome riding the head row because there is nothing between
|
|
813
|
+
// them to close.
|
|
776
814
|
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
return out;
|
|
815
|
+
// DECLARED REVERSAL of the owner's 2026-09-02 narrowing ("a call
|
|
816
|
+
// with no output on screen has no slab at all"), which read §1.6
|
|
817
|
+
// as giving the wash to the machine's VERBATIM text only. The
|
|
818
|
+
// ruling of 2026-09-03 supersedes it: the surface says WORK, not
|
|
819
|
+
// VERBATIM, and a page where some calls are cards and others are
|
|
820
|
+
// loose rows is the instability the owner was pointing at. §1.6
|
|
821
|
+
// moves with it.
|
|
822
|
+
const bare = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
823
|
+
return slabBlock(appendSuffix(bare, expandSuffix(hidden, W - visibleWidth(bare))), [], null, W);
|
|
787
824
|
}
|
|
788
825
|
if (c.state === "approval") {
|
|
789
826
|
// W2: the ❯ is the GUTTER (the left edge), never the line's tail
|
|
790
827
|
const out = gutterCut(`${p.bold}❯${p.reset} `, `${verbCol} ${liveTarget(c)}`, W);
|
|
791
|
-
out.push(...toolBlockBody(c, W));
|
|
828
|
+
out.push(...toolBlockBody(c, W, ctx));
|
|
792
829
|
return out;
|
|
793
830
|
}
|
|
794
831
|
if (c.state === "running") {
|
|
@@ -809,10 +846,68 @@ class ToolExecution {
|
|
|
809
846
|
// cannot be predicted: a turning mark implies progress the
|
|
810
847
|
// product does not have. With no ground the breath freezes to a
|
|
811
848
|
// static `●` and says the same thing more quietly.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
849
|
+
// R13 E2 — A RUNNING CALL IS THE SAME CARD, allocated at the
|
|
850
|
+
// SETTLED card's height from its first frame. The settle then
|
|
851
|
+
// changes CONTENT and never position: the spinner becomes two
|
|
852
|
+
// spaces in the same two columns, the live window gives back
|
|
853
|
+
// the rows the result did not need, and the metadata row that
|
|
854
|
+
// said `3s` says `exit 0 · 90 lines · 3.2s`.
|
|
855
|
+
//
|
|
856
|
+
// The elapsed moves OFF the head row onto that metadata row,
|
|
857
|
+
// which is where the settled card keeps it — the head row's job
|
|
858
|
+
// is to say what is running, and it says the same thing before
|
|
859
|
+
// and after.
|
|
860
|
+
//
|
|
861
|
+
// DC-43: with too little room for the seven-row skeleton (two
|
|
862
|
+
// pads, the head, two blanks, one preview row and the metadata)
|
|
863
|
+
// there is no card — the call keeps its head row until it
|
|
864
|
+
// commits, which is the one form that fits anywhere.
|
|
865
|
+
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
866
|
+
const gutter = ctx.grouped === true ? " " : `${breathFrame(ctx.spinnerI)} `;
|
|
867
|
+
if (liveRows <= 0) {
|
|
868
|
+
// the degraded form is the head row ALONE, so it keeps the
|
|
869
|
+
// duration it would otherwise have lost with its card — cut
|
|
870
|
+
// against the room the duration leaves (VD-4: the duration
|
|
871
|
+
// is its own segment, never welded to a cut word).
|
|
872
|
+
const bare = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
|
|
873
|
+
return [`${bare[0]}${p.dim}${dur}${p.reset}`];
|
|
874
|
+
}
|
|
875
|
+
// DC-46 — the two GESTURES ride the status row, where they cost
|
|
876
|
+
// nothing. They were a footer INSIDE the window, spending one of
|
|
877
|
+
// its rows on a sentence that is not output; with the window
|
|
878
|
+
// grown from its content, that row was the difference between a
|
|
879
|
+
// settle that swaps content and one that changes height. The
|
|
880
|
+
// row's shape is the settled outcome row's, so the settle
|
|
881
|
+
// rewrites it in place: `3s · esc stops` → `exit 0 · 90 lines ·
|
|
882
|
+
// 3.2s`.
|
|
883
|
+
const gestures = c.name === "shell" ? " · esc stops · alt+⏎ redirects" : "";
|
|
884
|
+
const status = pickTier([`${elapsed}s${gestures}`, `${elapsed}s`], Math.max(1, W - visibleWidth(noteIndent())));
|
|
885
|
+
const live = toolBlockBody(c, W, ctx);
|
|
886
|
+
if (live.length > 0)
|
|
887
|
+
return slabBlock(gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, W)[0], live, status, W);
|
|
888
|
+
// DC-48 — THE THREE-ROW CARD IS ONE ROW, so it is assembled here
|
|
889
|
+
// against the room it actually has.
|
|
890
|
+
//
|
|
891
|
+
// This branch used to cut the head to `W` and hand it to
|
|
892
|
+
// `slabBlock`, which joins head and outcome and cut nothing —
|
|
893
|
+
// so the row came out `W` wide PLUS the whole status, and the
|
|
894
|
+
// compositor did what it promises: it threw. On the owner's
|
|
895
|
+
// 80-column terminal a long `find` produced a 113-column row on
|
|
896
|
+
// its FIRST FRAME, which is the first second of every command.
|
|
897
|
+
//
|
|
898
|
+
// Pin 4's order: the command is the cuttable span and the
|
|
899
|
+
// elapsed is never cut open, so the command takes what the
|
|
900
|
+
// status leaves — the `· ` between them and the two-column
|
|
901
|
+
// gutter included.
|
|
902
|
+
// the status takes its own tier against the room the row has, not
|
|
903
|
+
// against a whole width: on a narrow terminal the gestures give
|
|
904
|
+
// way so the COMMAND keeps something to say, and the elapsed —
|
|
905
|
+
// pin 4's core — never does.
|
|
906
|
+
const MIN_TARGET = 10; // the gutter, the verb column, a character of command
|
|
907
|
+
const oneRow = pickTier([`${elapsed}s${gestures}`, `${elapsed}s`], Math.max(1, W - MIN_TARGET - 3));
|
|
908
|
+
const room = Math.max(4, W - visibleWidth(oneRow) - 3);
|
|
909
|
+
const only = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, room)[0];
|
|
910
|
+
return slabBlock(`${only}${p.dim} · ${oneRow}${p.reset}`, [], null, W);
|
|
816
911
|
}
|
|
817
912
|
// W2: ◦ replaces → for QUEUED — · is the separator inside every
|
|
818
913
|
// metadata group; a queued marker that is also the separator
|
|
@@ -839,7 +934,7 @@ class ToolExecution {
|
|
|
839
934
|
* never a cap.
|
|
840
935
|
*/
|
|
841
936
|
function hiddenLines(c, W) {
|
|
842
|
-
if (c.expanded || c.state !== "done" || c.
|
|
937
|
+
if (c.expanded || c.state !== "done" || c.reason !== null)
|
|
843
938
|
return null;
|
|
844
939
|
if (c.name === "delegate")
|
|
845
940
|
return null; // its body is the one-line summary, always whole
|
|
@@ -848,13 +943,14 @@ function hiddenLines(c, W) {
|
|
|
848
943
|
return null;
|
|
849
944
|
if (c.isError)
|
|
850
945
|
return null; // errorBody's own cut row is the affordance there
|
|
851
|
-
//
|
|
852
|
-
//
|
|
946
|
+
// R13: a call that PREVIEWS carries the key on its own note row when
|
|
947
|
+
// something is cut, and needs no affordance at all when nothing is.
|
|
853
948
|
// A head-row suffix as well would be TUI2-R1's two affordances for
|
|
854
|
-
// one cell — the thing that rule exists to forbid.
|
|
855
|
-
|
|
949
|
+
// one cell — the thing that rule exists to forbid. read_file is the
|
|
950
|
+
// one call with no preview (E1), so the key lives on its head row.
|
|
951
|
+
if (c.name !== "read_file")
|
|
856
952
|
return null;
|
|
857
|
-
return n;
|
|
953
|
+
return n;
|
|
858
954
|
}
|
|
859
955
|
/**
|
|
860
956
|
* TUI2-R1 (A) — the suffix, in the width that is LEFT.
|
|
@@ -900,12 +996,24 @@ const SUFFIX_MIN = " · ctrl+o".length;
|
|
|
900
996
|
* compressible thing on the row — a reader recognises a command
|
|
901
997
|
* from its head — and it is the only part with a natural ellipsis.
|
|
902
998
|
*/
|
|
903
|
-
function settledHeadText(verbCol, target, meta, attr, elapsed, room) {
|
|
904
|
-
|
|
905
|
-
|
|
999
|
+
function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "") {
|
|
1000
|
+
// R13 — ONE GRAMMAR FOR BOTH CARDS. This row used to close with W4's
|
|
1001
|
+
// parentheses — `read a.ts (0.1s) · 10 lines · ctrl+o expands` —
|
|
1002
|
+
// while the bodied card's outcome row said `exit 0 · 90 lines · 0.4s`
|
|
1003
|
+
// in a `·` chain. Two shapes for the same facts, and which one a call
|
|
1004
|
+
// got depended on whether it happened to have a preview. The chain
|
|
1005
|
+
// wins: it is the one the outcome row already uses, it reads in one
|
|
1006
|
+
// direction, and it puts the elapsed where the other card puts it.
|
|
1007
|
+
//
|
|
1008
|
+
// The giving-way order is pin 4's, unchanged: the ATTRIBUTION drops
|
|
1009
|
+
// first, then the count, and the core — what happened and how long it
|
|
1010
|
+
// took — is never cut open; below that the target itself truncates.
|
|
1011
|
+
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
1012
|
+
const core = join(meta, counted, `${elapsed}s`);
|
|
1013
|
+
const withAttr = join(meta, counted, `${elapsed}s`, attr.replace(" · ", ""));
|
|
906
1014
|
const lead = `${verbCol} `;
|
|
907
|
-
const fit = (t,
|
|
908
|
-
const line = `${lead}${t}${
|
|
1015
|
+
const fit = (t, tail) => {
|
|
1016
|
+
const line = `${lead}${t}${tail === "" ? "" : ` · ${tail}`}`;
|
|
909
1017
|
return visibleWidth(line) <= room ? line : null;
|
|
910
1018
|
};
|
|
911
1019
|
// 1. everything
|
|
@@ -916,12 +1024,26 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room) {
|
|
|
916
1024
|
const bare = fit(target, core);
|
|
917
1025
|
if (bare !== null)
|
|
918
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;
|
|
919
1032
|
// 3. the target truncates, the core stays whole
|
|
920
|
-
const
|
|
1033
|
+
const stem = join(meta, `${elapsed}s`);
|
|
1034
|
+
const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
|
|
921
1035
|
if (budget >= 1)
|
|
922
|
-
return `${lead}${widthCut(target, budget)}… ${
|
|
923
|
-
// 4.
|
|
924
|
-
//
|
|
1036
|
+
return `${lead}${widthCut(target, budget)}… · ${stem}`;
|
|
1037
|
+
// 4. DC-48 — the ELAPSED still rides, and the target takes what is
|
|
1038
|
+
// left. This used to return the target alone, on the argument that
|
|
1039
|
+
// a cut core would leave a half-open parenthesis; R13's chain has
|
|
1040
|
+
// no bracket to leave open, so the reason retired with the
|
|
1041
|
+
// parentheses and pin 4's own rule applies at every width: what
|
|
1042
|
+
// happened and how long it took is never cut away.
|
|
1043
|
+
const floor = `${elapsed}s`;
|
|
1044
|
+
const left = room - visibleWidth(lead) - visibleWidth(floor) - 4; // the ellipsis + " · "
|
|
1045
|
+
if (left >= 1)
|
|
1046
|
+
return `${lead}${widthCut(target, left)}… · ${floor}`;
|
|
925
1047
|
return `${lead}${widthCut(target, Math.max(1, room - visibleWidth(lead)))}`;
|
|
926
1048
|
}
|
|
927
1049
|
/**
|
|
@@ -947,8 +1069,13 @@ function attribution(c) {
|
|
|
947
1069
|
export function expandSuffix(lines, room) {
|
|
948
1070
|
if (lines === null)
|
|
949
1071
|
return "";
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
for (const tier of [" · ctrl+o expands", " · ctrl+o"]) {
|
|
952
1079
|
if (tier.length <= room)
|
|
953
1080
|
return tier;
|
|
954
1081
|
}
|
|
@@ -1051,58 +1178,6 @@ const EXPLORE_NOUN = {
|
|
|
1051
1178
|
export function isExploreTool(name) {
|
|
1052
1179
|
return EXPLORE_NOUN[name] !== undefined;
|
|
1053
1180
|
}
|
|
1054
|
-
/** "8 files · 14 searches" — the per-tool counts in first-call order. */
|
|
1055
|
-
export function exploreCounts(parts) {
|
|
1056
|
-
return parts
|
|
1057
|
-
.map((part) => {
|
|
1058
|
-
const [singular, plural] = EXPLORE_NOUN[part.name] ?? ["call", "calls"];
|
|
1059
|
-
// R3h (fable, 2026-08-29): DISTINCT subjects. This counted calls
|
|
1060
|
-
// while exploreRows — the very next function, the expansion of
|
|
1061
|
-
// THIS row — deduped them with `×N`. So the head said "6 files"
|
|
1062
|
-
// over a list showing four, one of them `a.ts ×3`. The head and
|
|
1063
|
-
// the body are two views of one run and must count alike; the
|
|
1064
|
-
// body was right (a file read twice is one file).
|
|
1065
|
-
const n = foldCountsObjects(part.name) ? new Set(part.subjects).size : part.subjects.length;
|
|
1066
|
-
return `${n} ${n === 1 ? singular : plural}`;
|
|
1067
|
-
})
|
|
1068
|
-
.join(" · ");
|
|
1069
|
-
}
|
|
1070
|
-
/** TUI2-R1 (B) — the expanded list: ONE row per tool, the verb column
|
|
1071
|
-
* then the distinct subjects in first-call order, a repeated subject
|
|
1072
|
-
* carrying its ×count, the first three shown and the rest counted.
|
|
1073
|
-
* A search's subject is its PATTERN (quoted — the thing that was
|
|
1074
|
-
* looked for); a read's or a list's is its path. */
|
|
1075
|
-
export function exploreRows(parts, W) {
|
|
1076
|
-
const p = palette();
|
|
1077
|
-
const rows = [];
|
|
1078
|
-
for (const part of parts) {
|
|
1079
|
-
const counts = new Map();
|
|
1080
|
-
for (const s of part.subjects)
|
|
1081
|
-
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
1082
|
-
const shown = [...counts.entries()].slice(0, 3).map(([s, n]) => (n > 1 ? `${s} ×${n}` : s));
|
|
1083
|
-
const more = counts.size > 3 ? ` (+${counts.size - 3})` : "";
|
|
1084
|
-
const verb = displayVerb(part.name);
|
|
1085
|
-
rows.push(cutLine(`${p.dim}${BODY_ROW}${escapeTerminal(`${verb.padEnd(6)} ${shown.join(" · ")}${more}`)}${p.reset}`, W));
|
|
1086
|
-
}
|
|
1087
|
-
// TUI2-R1.5 ① (VD-15): the footer used to promise "/last shows the full
|
|
1088
|
-
// outputs". /last shows the LAST call only — for a nine-call burst that
|
|
1089
|
-
// is one output out of nine, and a footer that sends the human to a
|
|
1090
|
-
// place the content is not is worse than a footer that says nothing.
|
|
1091
|
-
// R8a: the footer is an in-block note — the same indent, no glyph —
|
|
1092
|
-
// and the corner opens the block's first row, like every other one.
|
|
1093
|
-
rows.push(cutLine(`${p.dim}${NOTE_ROW}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1094
|
-
return openBlock(rows);
|
|
1095
|
-
}
|
|
1096
|
-
/** The count term with the singular/plural forms — "no reads", "1 read",
|
|
1097
|
-
* "5 reads". The noun's singular drops the plural suffix ("dirs" → "dir",
|
|
1098
|
-
* "matches" → "match"). */
|
|
1099
|
-
function countTerm(n, singular, plural) {
|
|
1100
|
-
if (n === 0)
|
|
1101
|
-
return `no ${plural}`;
|
|
1102
|
-
if (n === 1)
|
|
1103
|
-
return `1 ${singular}`;
|
|
1104
|
-
return `${n} ${plural}`;
|
|
1105
|
-
}
|
|
1106
1181
|
/** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
|
|
1107
1182
|
* scrollback, becomes ONE line — the work order's claimed shape
|
|
1108
1183
|
* (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
|
|
@@ -1340,252 +1415,19 @@ function stretchTerms(t, live) {
|
|
|
1340
1415
|
return `${e[tense(name)]} ${n} ${n === 1 ? e[2] : e[3]}`;
|
|
1341
1416
|
});
|
|
1342
1417
|
}
|
|
1343
|
-
/** R3i — the trouble clause: which call, and what happened, in WORDS.
|
|
1344
|
-
* Law 1.3 says an outcome is stated in words, "the only form that
|
|
1345
|
-
* survives a pipe"; the colour on this clause is emphasis over those
|
|
1346
|
-
* words, never the fact itself. */
|
|
1347
|
-
function troubleClause(t) {
|
|
1348
|
-
return t.trouble
|
|
1349
|
-
.filter(([, n]) => n > 0)
|
|
1350
|
-
.map(([kind, n, what]) => (what === "" || kind === "interrupted" ? `${n} ${kind}` : `${n} ${kind}: ${what}`))
|
|
1351
|
-
.join(" · ");
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* R4 (C1) — the fold NAMES ITS OWN TARGET.
|
|
1355
|
-
*
|
|
1356
|
-
* `ctrl+o` used to be printed identically on every fold on the screen,
|
|
1357
|
-
* and the key walked a ring whose order nothing on screen expressed —
|
|
1358
|
-
* so the owner's report was exact: "there is no way to know which
|
|
1359
|
-
* stretch it opens". The tint that marks the next target can only be
|
|
1360
|
-
* drawn on a LIVE row, and every fold worth reopening is, by
|
|
1361
|
-
* construction, in the scrollback where nothing can be tinted.
|
|
1362
|
-
*
|
|
1363
|
-
* A pointer cannot fix this either, and the bound is worth stating
|
|
1364
|
-
* once: SGR mouse reports address the VIEWPORT, so a fold that has
|
|
1365
|
-
* scrolled into the terminal's own scrollback is unreachable by any
|
|
1366
|
-
* pointer, permanently, on the primary screen. The ordinal is not a
|
|
1367
|
-
* cheaper substitute for clicking — it is the form of the affordance
|
|
1368
|
-
* that reaches every fold, and it survives a pipe as characters.
|
|
1369
|
-
*
|
|
1370
|
-
* The number rides the KEY, inside the width ladder, so it is paid for
|
|
1371
|
-
* by the same give-way order as every other span (law: the key never
|
|
1372
|
-
* gives way — it just got two characters longer).
|
|
1373
|
-
*/
|
|
1374
|
-
export function stretchLine(t, W) {
|
|
1375
|
-
const p = palette();
|
|
1376
|
-
const live = t.phase !== "settled";
|
|
1377
|
-
// DECLARED SUPERSESSION (R6/D3, owner-ruled) — THE STRETCH LINE WEARS
|
|
1378
|
-
// NO MARK, in any phase.
|
|
1379
|
-
//
|
|
1380
|
-
// Law 1.3: a symbol earns its cell by carrying a fact the words do
|
|
1381
|
-
// not. When every settled fold, the live line AND the status row all
|
|
1382
|
-
// wear a star, none of them distinguishes anything — it is the tick
|
|
1383
|
-
// and the cross again (R2 retired those on exactly this ground), at
|
|
1384
|
-
// the stretch scale. design.md §7.4 had already ruled the principle
|
|
1385
|
-
// one scale down: "only the call still running carries a mark,
|
|
1386
|
-
// because only it is moving", with a settled call's mark "(none) —
|
|
1387
|
-
// the outcome is in the words. SETTLED." A settled STRETCH wearing
|
|
1388
|
-
// one contradicted a precedent the file had already ratified.
|
|
1389
|
-
//
|
|
1390
|
-
// Nothing settled is being reversed: §4 lists this mark PROPOSED and
|
|
1391
|
-
// §8 lists it OPEN. This is that proposal's ruling arriving, as a
|
|
1392
|
-
// decline, on the owner's own dogfood.
|
|
1393
|
-
//
|
|
1394
|
-
// The replacement is a two-space INDENT, not a column shift: the row
|
|
1395
|
-
// joins the settled-call family's geometry, the indent survives a
|
|
1396
|
-
// pipe as bytes (prose never starts at column 3), and both forms are
|
|
1397
|
-
// 2 cells so the width ladder below does not reflow. The status row's
|
|
1398
|
-
// twinkle survives as the ONE moving mark; `✦ took` survives as the
|
|
1399
|
-
// turn's seal.
|
|
1400
|
-
//
|
|
1401
|
-
// R7a AMENDS this by ONE case: the live ACTING line takes the
|
|
1402
|
-
// breathing mark, passed in. D3 declined a mark on the SETTLED fold,
|
|
1403
|
-
// where the words already carry the outcome; a line that means "work
|
|
1404
|
-
// is in flight RIGHT NOW" carries a fact its words do not, which is
|
|
1405
|
-
// exactly the test law 1.3 sets. It is also where the mark was
|
|
1406
|
-
// migrating TO: §7.4's "only the call still running carries one" now
|
|
1407
|
-
// applies at the stretch scale, one mark for the activity instead of
|
|
1408
|
-
// one per call. Owner-ruled 2026-08-31, on the ground that a fast
|
|
1409
|
-
// call's per-row mark is gone before the eye lands.
|
|
1410
|
-
const mark = t.mark ?? " ";
|
|
1411
|
-
// R4a (owner ruling, 2026-08-30) — the fold row prints NO key.
|
|
1412
|
-
//
|
|
1413
|
-
// R4 printed `· ctrl+o 3` so the row could name its own target. The
|
|
1414
|
-
// owner's objection is the right one: a number you cannot type is not
|
|
1415
|
-
// a selector, it is decoration that costs a column — and the
|
|
1416
|
-
// reference implementation, checked rather than assumed, prints
|
|
1417
|
-
// nothing on the row either. Its expansion lives in a MODE you enter,
|
|
1418
|
-
// where the pointer can reach every fold including the ones that have
|
|
1419
|
-
// scrolled away; the row itself stays clean.
|
|
1420
|
-
//
|
|
1421
|
-
// So the affordance is retired here and owed to that mode. Until it
|
|
1422
|
-
// exists, `ctrl+o` still opens the most recent fold — it is simply no
|
|
1423
|
-
// longer advertised on a row that cannot say which one it means.
|
|
1424
|
-
const key = "";
|
|
1425
|
-
const lead = t.phase === "thinking" ? [`thinking ${t.thoughtSeconds}s`] : t.phase === "settled" && t.thoughtSeconds > 0 ? [`thought ${t.thoughtSeconds}s`] : [];
|
|
1426
|
-
const clauseText = troubleClause(t);
|
|
1427
|
-
let meta = [...lead, ...(t.phase === "thinking" ? [] : stretchTerms(t, live))].join(" · ");
|
|
1428
|
-
let clause = clauseText === "" ? "" : ` · ${clauseText}`;
|
|
1429
|
-
let words = t.words === undefined || t.words === "" ? "" : ` ${escapeTerminal(t.words).replace(/\s+/g, " ")} `;
|
|
1430
|
-
const width = () => 2 + (words === "" ? 0 : visibleWidth(words) + 3) + visibleWidth(meta) + visibleWidth(clause) + visibleWidth(key);
|
|
1431
|
-
const trim = (text, room, floor) => (room >= floor ? `${widthCut(text, room)}…` : "");
|
|
1432
|
-
if (words !== "" && width() > W)
|
|
1433
|
-
words = trim(words, W - (width() - visibleWidth(words)) - 1, 4);
|
|
1434
|
-
if (width() > W) {
|
|
1435
|
-
for (const [long, short] of STRETCH_COMPACT) {
|
|
1436
|
-
meta = meta.replaceAll(long, short);
|
|
1437
|
-
if (width() <= W)
|
|
1438
|
-
break;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (width() > W) {
|
|
1442
|
-
const room = W - (width() - visibleWidth(meta)) - 1;
|
|
1443
|
-
meta = room >= 2 ? `${widthCut(meta, room)}…` : "…";
|
|
1444
|
-
}
|
|
1445
|
-
if (width() > W && clause !== "")
|
|
1446
|
-
clause = trim(clause, W - (width() - visibleWidth(clause)) - 1, 5);
|
|
1447
|
-
// the degenerate floor: below the width where even the mark and one
|
|
1448
|
-
// character fit, the row is a hard cut of what it would have said.
|
|
1449
|
-
const row = `${mark}${words === "" ? "" : ` ${p.rv}${words}${p.rvEnd}${p.dim} ·${p.reset}`} ${meta}${clause === "" ? "" : `${p.red}${clause}${p.reset}`}${key === "" ? "" : `${p.dim}${key}${p.reset}`}`;
|
|
1450
|
-
return [visibleWidth(row) <= W ? row : cutLine(row, W)];
|
|
1451
|
-
}
|
|
1452
|
-
/** R6/D3: the quiet turn's fold wears no mark either — the SECOND
|
|
1453
|
-
* emission site, and the one the D3 brief did not name. Same ruling,
|
|
1454
|
-
* same two-space indent; see stretchLine above for the argument. */
|
|
1455
|
-
export function turnFold(t, W) {
|
|
1456
|
-
const p = palette();
|
|
1457
|
-
// R3b (owner, 2026-08-27): ZERO TERMS ARE DROPPED. W14 always wrote
|
|
1458
|
-
// `no reads · no edits`, which is a sentence about things that did not
|
|
1459
|
-
// happen — on a segment fold, where a run is usually all reads or all
|
|
1460
|
-
// edits, half the row was the half that said nothing. A term earns its
|
|
1461
|
-
// place by having a count.
|
|
1462
|
-
// R3h (fable, 2026-08-29): the THOUGHT term obeys the same zero-drop
|
|
1463
|
-
// rule every other term got at R3b. It was exempt by accident — it
|
|
1464
|
-
// was written before the rule — so a model that emits no thinking
|
|
1465
|
-
// folded every turn of its life under `thought 0s`, a sentence about
|
|
1466
|
-
// something that did not happen, in the lead position.
|
|
1467
|
-
const meta = [...(t.thoughtSeconds > 0 ? [`thought ${t.thoughtSeconds}s`] : []), ...foldTerms(t.reads, t.edits, t.others)].join(" · ");
|
|
1468
|
-
// R3f: the same one-row rule. This one is worse than the thinking
|
|
1469
|
-
// fold's — the fold is a COMMITTED row, so a multi-line user message
|
|
1470
|
-
// would desync the committed-line accounting permanently rather than
|
|
1471
|
-
// for one frame. (The live UserMessage chip keeps its multi-row form:
|
|
1472
|
-
// it folds per paragraph and each row is already one row.)
|
|
1473
|
-
const words = escapeTerminal(t.words).replace(/\s+/g, " ");
|
|
1474
|
-
// R3g (2026-08-28) — THE KEY IS LOAD-BEARING, SO IT GIVES WAY LAST.
|
|
1475
|
-
//
|
|
1476
|
-
// R3b wrote "the suffix gives way first at a narrow width", by
|
|
1477
|
-
// analogy with the settled card's affordance. The analogy does not
|
|
1478
|
-
// hold: the card's rows are still on the screen when its hint is
|
|
1479
|
-
// dropped, and a fold's are not. A fold with no key is the turn's
|
|
1480
|
-
// work behind a line with no way back — the one thing the fold's own
|
|
1481
|
-
// header says it must never be. So the META gives way first (a cut
|
|
1482
|
-
// count is still a true count), and the key survives.
|
|
1483
|
-
//
|
|
1484
|
-
// Two live cases, both reachable at W=80: the wordless fold went
|
|
1485
|
-
// keyless as soon as the terms grew (R3g's verb+noun phrasing pushed
|
|
1486
|
-
// it over), and the CHIP fold never had a key at all — it was
|
|
1487
|
-
// written without one, so every folded turn that carried the user's
|
|
1488
|
-
// words was unreachable by the key its siblings advertise. Found by
|
|
1489
|
-
// an independent review (fable) and then by the paced-rollup gate,
|
|
1490
|
-
// which got `kind: "none"` back from expandNext.
|
|
1491
|
-
// R3h: the COMPACT tier. The owner's own sentence — "thought 17s ·
|
|
1492
|
-
// read 4 files · listed 1 directory · ran 4 shell commands" — is 81
|
|
1493
|
-
// cells with the key at W=80: one over, so the tail was cut off the
|
|
1494
|
-
// exact shape this round exists to produce. The nouns shorten before
|
|
1495
|
-
// anything is lost, which is the same "degrade, never truncate"
|
|
1496
|
-
// ladder every other row here walks.
|
|
1497
|
-
// A LADDER, cheapest first, and it stops as soon as the row fits: the
|
|
1498
|
-
// owner's canonical sentence is ONE cell over at W=80, and spending
|
|
1499
|
-
// every substitution to buy one cell would shorten words that had
|
|
1500
|
-
// room. "directory" is the cheapest to lose (nobody misreads "dir");
|
|
1501
|
-
// "commands" is next and still says what happened.
|
|
1502
|
-
const COMPACT = [
|
|
1503
|
-
["directories", "dirs"],
|
|
1504
|
-
["directory", "dir"],
|
|
1505
|
-
["shell commands", "commands"],
|
|
1506
|
-
["shell command", "command"],
|
|
1507
|
-
];
|
|
1508
|
-
const compactAll = (text) => {
|
|
1509
|
-
let out = text;
|
|
1510
|
-
for (const [long, short] of COMPACT)
|
|
1511
|
-
out = out.replaceAll(long, short);
|
|
1512
|
-
return out;
|
|
1513
|
-
};
|
|
1514
|
-
const KEY = " · ctrl+o";
|
|
1515
|
-
const keyW = KEY.length;
|
|
1516
|
-
const key = `${p.dim}${KEY}${p.reset}`;
|
|
1517
|
-
if (words === "") {
|
|
1518
|
-
const keyed = ` ${meta}${key}`;
|
|
1519
|
-
if (visibleWidth(keyed) <= W)
|
|
1520
|
-
return [keyed];
|
|
1521
|
-
let tightMeta = meta;
|
|
1522
|
-
for (const [long, short] of COMPACT) {
|
|
1523
|
-
tightMeta = tightMeta.replaceAll(long, short);
|
|
1524
|
-
const tight = ` ${tightMeta}${key}`;
|
|
1525
|
-
if (visibleWidth(tight) <= W)
|
|
1526
|
-
return [tight];
|
|
1527
|
-
}
|
|
1528
|
-
// the meta cuts with the honest "…" — the COMPACT form of it, so a
|
|
1529
|
-
// cut row still carries as many whole terms as the width allows;
|
|
1530
|
-
// below the width where even the key fits there is nothing to
|
|
1531
|
-
// preserve and the row is a cut.
|
|
1532
|
-
const room = W - 2 - keyW - 1;
|
|
1533
|
-
if (room < 2) {
|
|
1534
|
-
// R3h: and below FOUR columns even that row is 4 cells wide (the
|
|
1535
|
-
// gutter, one character, the "…"), so invariant ① threw AT the
|
|
1536
|
-
// widths this branch exists to serve — the same class DC-15
|
|
1537
|
-
// closed in the thinking fold, still open here. A width with no
|
|
1538
|
-
// room for the mark is a width with nothing to say: the row is
|
|
1539
|
-
// a hard cut of what it would have said.
|
|
1540
|
-
const cut = ` ${widthCut(tightMeta, Math.max(1, W - 3))}…`;
|
|
1541
|
-
return [visibleWidth(cut) <= W ? cut : cutLine(` ${tightMeta}`, W)];
|
|
1542
|
-
}
|
|
1543
|
-
return [` ${widthCut(tightMeta, room)}…${key}`];
|
|
1544
|
-
}
|
|
1545
|
-
// A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
1546
|
-
// words LEAD the one line, the same SGR-7 bracket as the live user
|
|
1547
|
-
// row (#16f, side pads included). The words take the fold's width
|
|
1548
|
-
// budget: W − the gutter (two spaces = 2, R6/D3) − the join (3) − the
|
|
1549
|
-
// chip's side pads (2) − the cut-tail reserve (1, the "…") − the
|
|
1550
|
-
// metadata's own width — the metadata survives, the words width-cut
|
|
1551
|
-
// at the end with the honest "…" (the "…" alone is the honest floor:
|
|
1552
|
-
// the words were there, cut).
|
|
1553
|
-
const budget = Math.max(0, W - visibleWidth(` ${meta}`) - 6 - keyW);
|
|
1554
|
-
const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
|
|
1555
|
-
const row = ` ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
|
|
1556
|
-
if (visibleWidth(row) <= W)
|
|
1557
|
-
return [row];
|
|
1558
|
-
// R3h: the same COMPACT ladder the wordless branch walks — the nouns
|
|
1559
|
-
// shorten before the words or the metadata lose anything.
|
|
1560
|
-
let chipMeta = meta;
|
|
1561
|
-
for (const [long, short] of COMPACT) {
|
|
1562
|
-
chipMeta = chipMeta.replaceAll(long, short);
|
|
1563
|
-
const tighter = ` ${p.rv} ${cut} ${p.rvEnd} · ${chipMeta}${key}`;
|
|
1564
|
-
if (visibleWidth(tighter) <= W)
|
|
1565
|
-
return [tighter];
|
|
1566
|
-
}
|
|
1567
|
-
// the last resort: the METADATA gives way — the words hold their
|
|
1568
|
-
// budget, the key holds its nine cells, the meta cuts with the honest
|
|
1569
|
-
// "…"; invariant ① never trips at ANY width (a degenerate W's fold is
|
|
1570
|
-
// a cut, never a crash).
|
|
1571
|
-
const room = W - 8 - keyW - visibleWidth(cut);
|
|
1572
|
-
if (room < 2) {
|
|
1573
|
-
// R3h: the chip branch's own degenerate floor. The gutter, the
|
|
1574
|
-
// bracket's pads, the join and the "…" are ten cells before a
|
|
1575
|
-
// single character of content, so every width below ten threw
|
|
1576
|
-
// invariant ① — see the wordless branch above for the same class.
|
|
1577
|
-
const tail = ` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), Math.max(1, W - 8 - visibleWidth(cut)))}…`;
|
|
1578
|
-
return [visibleWidth(tail) <= W ? tail : cutLine(` ${p.rv} ${cut} ${p.rvEnd}`, W)];
|
|
1579
|
-
}
|
|
1580
|
-
return [` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), room)}…${key}`];
|
|
1581
|
-
}
|
|
1582
1418
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1583
1419
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
1584
1420
|
* (the W7 table). The renderer-cut row is inside the cap. */
|
|
1585
|
-
|
|
1586
|
-
|
|
1421
|
+
/** R13 — ONE preview cap, every tool. It was the shell's alone while
|
|
1422
|
+
* the shell was the only settled call with rows on screen. */
|
|
1423
|
+
export const CAP_PREVIEW = 5;
|
|
1424
|
+
/** DC-46 — the running window's ceiling is the SETTLED preview's, and a
|
|
1425
|
+
* running card reaches it by growing rather than by being handed it.
|
|
1426
|
+
* `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;
|
|
1587
1430
|
const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
1588
|
-
const CAP_ERROR = 3; // the error text head
|
|
1589
1431
|
/** The block body rows' prefixes (W2's gutter table): │ a bounded
|
|
1590
1432
|
* block's body, └ the block's last row — what was cut, where the rest
|
|
1591
1433
|
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
@@ -1600,13 +1442,35 @@ const CAP_ERROR = 3; // the error text head
|
|
|
1600
1442
|
* prose (2). Bytes still tell them apart; no column of glyphs.
|
|
1601
1443
|
*
|
|
1602
1444
|
* `└` survives as the mark that OPENS the block, once, on its first
|
|
1603
|
-
* row (see openBlock). In-block notes take
|
|
1445
|
+
* row (see openBlock). In-block notes take the same indent,
|
|
1604
1446
|
* no glyph — because a second `└` inside one block would be the same
|
|
1605
1447
|
* mark meaning two things (§4.1). CUT_ROW is unchanged for the
|
|
1606
1448
|
* surfaces that are not a tool block: the fold row's target list, the
|
|
1607
1449
|
* slot's overflow count. */
|
|
1608
|
-
|
|
1609
|
-
|
|
1450
|
+
/** R8a's four columns — off the surface. Inside a painted card every
|
|
1451
|
+
* row sits at column 2 (R13 E4): the head row and the outcome row
|
|
1452
|
+
* bracket the preview, so the indent is no longer what says "these
|
|
1453
|
+
* rows are output". Where nothing paints, it is exactly that, and R8a
|
|
1454
|
+
* stands unchanged. */
|
|
1455
|
+
const BODY_ROW_FLAT = " ";
|
|
1456
|
+
const NOTE_ROW_FLAT = " ";
|
|
1457
|
+
const CARD_ROW = " ";
|
|
1458
|
+
/** R13 E3 — the column the model's words begin in, the same one the
|
|
1459
|
+
* card's rows and the chip's text begin in. */
|
|
1460
|
+
const PROSE_COL = " ";
|
|
1461
|
+
/** DC-47, ADJUDICATED — the model's THINKING begins in the SAME column
|
|
1462
|
+
* as everything else: two.
|
|
1463
|
+
*
|
|
1464
|
+
* It went to four when E3 moved prose to two, so that stripping the
|
|
1465
|
+
* escapes would still tell them apart (§1.2). The owner looked at it
|
|
1466
|
+
* and ruled against it: "the thinking area is not indented by the same
|
|
1467
|
+
* two as the first line — it needs to keep the same first-line indent
|
|
1468
|
+
* as everything else" (2026-09-04). §1.8's one left edge outranks the
|
|
1469
|
+
* distinction, and §1.2 takes a DECLARED EXCEPTION for this one pair —
|
|
1470
|
+
* see design.md §1.2 and §7.2 for what is given up and what is not. */
|
|
1471
|
+
const THINK_COL = " ";
|
|
1472
|
+
const bodyRow = () => (slabPaints() ? CARD_ROW : BODY_ROW_FLAT);
|
|
1473
|
+
const noteIndent = () => (slabPaints() ? CARD_ROW : NOTE_ROW_FLAT);
|
|
1610
1474
|
const CUT_ROW = "└ ";
|
|
1611
1475
|
/**
|
|
1612
1476
|
* R9 P2 — THE SLAB: a single call's block is one washed object.
|
|
@@ -1665,7 +1529,7 @@ function noteRow(text, W, tone) {
|
|
|
1665
1529
|
// its own. The row names a fact; a cut names it shorter.
|
|
1666
1530
|
const open = tone === "body" ? p.washDim : p.dim;
|
|
1667
1531
|
const close = tone === "body" ? p.washDimEnd : p.reset;
|
|
1668
|
-
return [cutLine(`${open}${
|
|
1532
|
+
return [cutLine(`${open}${noteIndent()}${text}${close}`, W)];
|
|
1669
1533
|
}
|
|
1670
1534
|
/**
|
|
1671
1535
|
* Assemble a call's block.
|
|
@@ -1682,18 +1546,29 @@ function slabBlock(head, body, outcome, W) {
|
|
|
1682
1546
|
out.push(...noteRow(outcome, W, "dim"));
|
|
1683
1547
|
return out;
|
|
1684
1548
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1549
|
+
// R13 — THE CARD. pad · head · blank · preview · blank · outcome ·
|
|
1550
|
+
// pad, and a call with nothing to preview is three rows with its
|
|
1551
|
+
// outcome riding the head. Every row sits at column 2 (E4): R8a put
|
|
1552
|
+
// a block's body at column 4 so a pipe could tell output from prose,
|
|
1553
|
+
// and inside a painted card the head and the outcome bracket it
|
|
1554
|
+
// instead — off the surface R8a's indent is still the fact, which is
|
|
1555
|
+
// why the degradation above keeps it.
|
|
1556
|
+
if (body.length === 0) {
|
|
1557
|
+
// DC-48: the join makes a ROW, so it is cut like every other row.
|
|
1558
|
+
// The callers above size their parts against the room they have;
|
|
1559
|
+
// this is the backstop that makes invariant ① hold whatever they
|
|
1560
|
+
// do, and it is what was missing when a running card's head was
|
|
1561
|
+
// cut to W and then had a whole status appended to it.
|
|
1562
|
+
const only = cutLine(outcome === null ? head : `${head} ${outcome}`, W);
|
|
1563
|
+
return [slabRow("", W), slabRow(only, W), slabRow("", W)];
|
|
1695
1564
|
}
|
|
1696
|
-
|
|
1565
|
+
const top = [slabRow("", W), slabRow(head, W), slabRow("", W), ...body.map((r) => slabRow(r, W))];
|
|
1566
|
+
// An EXPANDED block closes with its own footer and passes no outcome;
|
|
1567
|
+
// a blank row and an empty metadata row under it would be §1.3's
|
|
1568
|
+
// empty mark twice over.
|
|
1569
|
+
if (outcome === null)
|
|
1570
|
+
return [...top, slabRow("", W)];
|
|
1571
|
+
return [...top, slabRow("", W), ...noteRow(outcome, W, "body").map((r) => slabRow(r, W)), slabRow("", W)];
|
|
1697
1572
|
}
|
|
1698
1573
|
/** R8a — stamp `└` on a block's FIRST row, after every slice and note
|
|
1699
1574
|
* has been assembled, so the mark is always on the first row actually
|
|
@@ -1703,25 +1578,26 @@ function openBlock(rows) {
|
|
|
1703
1578
|
// or a blank leading output line can put an empty row first, and a
|
|
1704
1579
|
// corner there would be a mark on a row with nothing to mark — law
|
|
1705
1580
|
// 1.3, which is the rule this whole change is serving.
|
|
1706
|
-
const i = rows.findIndex((r) => visibleWidth(r) > visibleWidth(
|
|
1581
|
+
const i = rows.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1707
1582
|
if (i < 0)
|
|
1708
1583
|
return rows;
|
|
1709
1584
|
const first = rows[i];
|
|
1710
|
-
const at = first.indexOf(
|
|
1585
|
+
const at = first.indexOf(BODY_ROW_FLAT);
|
|
1711
1586
|
if (at < 0)
|
|
1712
1587
|
return rows;
|
|
1713
1588
|
// the corner REPLACES two of the four indent columns, so the text
|
|
1714
1589
|
// stays in the same column as every other row of the block.
|
|
1715
|
-
return [...rows.slice(0, i), `${first.slice(0, at)} \u2514 ${first.slice(at +
|
|
1590
|
+
return [...rows.slice(0, i), `${first.slice(0, at)} \u2514 ${first.slice(at + BODY_ROW_FLAT.length)}`, ...rows.slice(i + 1)];
|
|
1716
1591
|
}
|
|
1717
1592
|
const blockMemo = new WeakMap();
|
|
1718
1593
|
/** The block's body rows below the header (memoized, W9). */
|
|
1719
|
-
function toolBlockParts(c, W) {
|
|
1594
|
+
function toolBlockParts(c, W, ctx) {
|
|
1720
1595
|
const memo = blockMemo.get(c);
|
|
1721
1596
|
// the SURFACE is part of the key: the same cell renders different rows
|
|
1722
1597
|
// painted and unpainted, and a ground resolved after the first frame
|
|
1723
1598
|
// would otherwise be served the pre-ground shape forever.
|
|
1724
|
-
const
|
|
1599
|
+
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
1600
|
+
const state = `${c.state}:${c.isError}:${c.name}:${c.expanded ? "x" : ""}:${slabPaints() ? "slab" : "flat"}:${liveRows}`;
|
|
1725
1601
|
const content = c.state === "approval" ? (c.diff ?? null) : c.resultText;
|
|
1726
1602
|
if (memo !== undefined && memo.width === W && memo.state === state && memo.content === content)
|
|
1727
1603
|
return memo;
|
|
@@ -1743,62 +1619,71 @@ function toolBlockParts(c, W) {
|
|
|
1743
1619
|
? errorBody(c, W)
|
|
1744
1620
|
: c.name === "delegate"
|
|
1745
1621
|
? delegateSettled(c, W)
|
|
1746
|
-
: //
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
//
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
: []
|
|
1622
|
+
: // R13 — EVERY settled call previews. The shell's tail is
|
|
1623
|
+
// VD-5's own cap and direction (the conclusion is at the
|
|
1624
|
+
// end); everything else shows its head, because that is
|
|
1625
|
+
// where its answer is. read_file is the single exception
|
|
1626
|
+
// (E1): its result is the file, kiso has nothing to add by
|
|
1627
|
+
// showing five lines of it, and the head row's key opens
|
|
1628
|
+
// the whole thing.
|
|
1629
|
+
noPreview(c)
|
|
1630
|
+
? []
|
|
1631
|
+
: c.name === "shell"
|
|
1632
|
+
? shellTail(c.resultText, W, slabPaints() ? "body" : "dim")
|
|
1633
|
+
: previewHead(c.resultText, W, slabPaints() ? "body" : "dim")
|
|
1759
1634
|
: c.state === "running"
|
|
1760
1635
|
? c.name === "delegate"
|
|
1761
1636
|
? delegateRunning(c, W)
|
|
1762
|
-
:
|
|
1763
|
-
|
|
1764
|
-
|
|
1637
|
+
: // R13 E1 — the call with no preview settled has none while
|
|
1638
|
+
// it runs either; its card is three rows the whole way.
|
|
1639
|
+
noPreview({ ...c, state: "done" })
|
|
1640
|
+
? []
|
|
1641
|
+
: liveWindow(c, W, liveCap(c, liveRows), slabPaints() ? "body" : "dim")
|
|
1765
1642
|
: c.state === "approval"
|
|
1766
1643
|
? diffBody(c.diff, W)
|
|
1767
1644
|
: [];
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
|
|
1645
|
+
// E1 governs the PREVIEW — five lines of a file kiso has nothing to
|
|
1646
|
+
// add to — not kiso's sentence about the result. `offset=201 for the
|
|
1647
|
+
// rest` is actionable and no other row says it (W10 pins exactly
|
|
1648
|
+
// that), so a read the TOOL capped takes one body row and a read it
|
|
1649
|
+
// did not takes none. That shape difference is information: the two
|
|
1650
|
+
// events are different, and the row is what says so.
|
|
1773
1651
|
const note = c.expanded ? null : toolCutNote(c.name, c.resultText);
|
|
1774
1652
|
if (note !== null)
|
|
1775
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1653
|
+
rows.push(...foldLine(`${p.dim}${noteIndent()}${note}${p.reset}`, W));
|
|
1776
1654
|
// TUI2-R1 (A): an EXPANDED block says how to put it back. The footer
|
|
1777
1655
|
// rides a block that HAS rows — an expanded delegate whose summary
|
|
1778
1656
|
// marker is missing renders nothing, and a lone footer under a head
|
|
1779
1657
|
// row would be an affordance for an empty block.
|
|
1780
1658
|
if (c.expanded && rows.length > 0)
|
|
1781
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1659
|
+
rows.push(...foldLine(`${p.dim}${noteIndent()}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1782
1660
|
// R9 P2: `└` opens a block that has no surface. Inside a slab the
|
|
1783
1661
|
// surface IS the container, and a corner in it is §1.3's empty mark
|
|
1784
1662
|
// one scale up — so the corner and the slab are alternatives, never
|
|
1785
1663
|
// both.
|
|
1786
1664
|
const opened = slabPaints() ? rows : openBlock(rows);
|
|
1787
|
-
const parts = { width: W, state, content, rows: opened
|
|
1665
|
+
const parts = { width: W, state, content, rows: opened };
|
|
1788
1666
|
blockMemo.set(c, parts);
|
|
1789
1667
|
return parts;
|
|
1790
1668
|
}
|
|
1791
|
-
/**
|
|
1792
|
-
*
|
|
1793
|
-
|
|
1794
|
-
|
|
1669
|
+
/** R13 E1 — the one settled call that previews NOTHING. A read's result
|
|
1670
|
+
* IS the file; five lines of it tell a reader less than the head row
|
|
1671
|
+
* already does, and the key opens the whole thing. (The reference
|
|
1672
|
+
* implementation makes the same call, for the same reason.) Everything
|
|
1673
|
+
* else previews: a shell its tail, the rest their head. */
|
|
1674
|
+
function noPreview(c) {
|
|
1675
|
+
return c.state === "done" && !c.expanded && !c.isError && c.reason === null && c.name === "read_file";
|
|
1676
|
+
}
|
|
1677
|
+
/** The block's body rows. */
|
|
1678
|
+
function toolBlockBody(c, W, ctx) {
|
|
1679
|
+
return toolBlockParts(c, W, ctx).rows;
|
|
1795
1680
|
}
|
|
1796
|
-
/** Fold result text into
|
|
1681
|
+
/** Fold result text into body rows (the block's own indent): escape,
|
|
1797
1682
|
* split, fold each line at W−prefix; trailing empty rows (the result's
|
|
1798
1683
|
* final newline) drop. */
|
|
1799
1684
|
function blockRows(text, W, tone = "dim") {
|
|
1800
1685
|
const p = palette();
|
|
1801
|
-
const textW = Math.max(1, W - visibleWidth(
|
|
1686
|
+
const textW = Math.max(1, W - visibleWidth(bodyRow()));
|
|
1802
1687
|
const rows = [];
|
|
1803
1688
|
// R9 P2: inside a SLAB the output rows are body strength, never dim —
|
|
1804
1689
|
// §2.1 bars dim from the wash (3.91:1 light, 4.35:1 dark) and these
|
|
@@ -1808,9 +1693,9 @@ function blockRows(text, W, tone = "dim") {
|
|
|
1808
1693
|
const close = tone === "dim" ? p.reset : "";
|
|
1809
1694
|
for (const raw of escapeTerminal(text).split("\n")) {
|
|
1810
1695
|
for (const row of foldLine(raw, textW))
|
|
1811
|
-
rows.push(`${open}${
|
|
1696
|
+
rows.push(`${open}${bodyRow()}${row}${close}`);
|
|
1812
1697
|
}
|
|
1813
|
-
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(
|
|
1698
|
+
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow()))
|
|
1814
1699
|
rows.pop();
|
|
1815
1700
|
return rows;
|
|
1816
1701
|
}
|
|
@@ -1820,23 +1705,43 @@ function blockRows(text, W, tone = "dim") {
|
|
|
1820
1705
|
* direction). */
|
|
1821
1706
|
function shellTail(text, W, tone = "dim") {
|
|
1822
1707
|
const rows = blockRows(text, W, tone);
|
|
1823
|
-
if (rows.length <=
|
|
1708
|
+
if (rows.length <= CAP_PREVIEW)
|
|
1824
1709
|
return rows;
|
|
1825
1710
|
// R9 P2 / D4: FIVE output rows, and the note is a row of its own. The
|
|
1826
1711
|
// pre-slab arithmetic spent one of the five on the cut note, because
|
|
1827
1712
|
// the note had nowhere else to live; the slab's metadata rows are not
|
|
1828
1713
|
// output and are not counted against the output's cap.
|
|
1829
|
-
const kept =
|
|
1714
|
+
const kept = CAP_PREVIEW;
|
|
1830
1715
|
// The note goes ABOVE the tail: it says what was cut, and what was
|
|
1831
1716
|
// cut is what came BEFORE these rows. One position on both surfaces —
|
|
1832
1717
|
// the surface degrades, the content shape does not.
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1718
|
+
return [...cutNote(rows.length - kept, "earlier", W, tone), ...rows.slice(rows.length - kept)];
|
|
1719
|
+
}
|
|
1720
|
+
/** R13 — the preview every OTHER settled call gets: the FIRST rows,
|
|
1721
|
+
* capped at five, the cut note BELOW them. A shell's conclusion is at
|
|
1722
|
+
* the bottom of its output, so its preview is the tail and its note
|
|
1723
|
+
* opens the block (shellTail above); a list, a search, a fetch all
|
|
1724
|
+
* answer at the top, so the note closes it. One rule, two directions,
|
|
1725
|
+
* and the direction follows where the answer is.
|
|
1726
|
+
*
|
|
1727
|
+
* This is the DECLARED REVERSAL of VD-5 for every tool but the shell
|
|
1728
|
+
* (0.22.0 reversed the shell alone): VD-5 collapsed a settled call to
|
|
1729
|
+
* its head row because ungrounded output rows owned the screen, and
|
|
1730
|
+
* the card is what changes that arithmetic — the rows are inside a
|
|
1731
|
+
* surface that says where the call begins and ends. */
|
|
1732
|
+
function previewHead(text, W, tone) {
|
|
1733
|
+
const rows = blockRows(text, W, tone);
|
|
1734
|
+
if (rows.length <= CAP_PREVIEW)
|
|
1735
|
+
return rows;
|
|
1736
|
+
return [...rows.slice(0, CAP_PREVIEW), ...cutNote(rows.length - CAP_PREVIEW, "more", W, tone)];
|
|
1737
|
+
}
|
|
1738
|
+
/** The preview's cut note, in one place so the head and the tail
|
|
1739
|
+
* directions cannot drift apart. The KEY is reserved (TUI2-R1.5 ⑤): a
|
|
1740
|
+
* row that says how much is hidden without saying how to see it is the
|
|
1741
|
+
* silence the affordance exists to remove. */
|
|
1742
|
+
function cutNote(cut, word, W, tone) {
|
|
1743
|
+
const n = `${cut} ${word} line${cut === 1 ? "" : "s"}`;
|
|
1744
|
+
return noteRow(pickTier([`… ${n} · ctrl+o expands`, `… ${n} · ctrl+o`, `… ${cut} · ctrl+o`, "· ctrl+o"], W - visibleWidth(noteIndent())), W, tone);
|
|
1840
1745
|
}
|
|
1841
1746
|
/** The error text head: the FIRST rows, capped at 3 — the answer is at
|
|
1842
1747
|
* the start (opencode's collapseToolOutput direction). The header row
|
|
@@ -1852,81 +1757,77 @@ function errorBody(c, W) {
|
|
|
1852
1757
|
// FULL content including the "[Permission denied] " prefix (never
|
|
1853
1758
|
// hide information — the folded body rides the pinned row).
|
|
1854
1759
|
const skipFirst = c.name === "shell" && /^exit \d+/.test(c.resultText) ? 0 : c.reason !== null && c.reason !== undefined ? 0 : 1;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
/** The running tool's FIXED-height window (W8): exactly 3 rows from
|
|
1862
|
-
* the FIRST frame — blank-padded before output arrives, the renderer
|
|
1863
|
-
* cut inside the window. The height changes exactly once, at settle —
|
|
1864
|
-
* a cell that grows mid-list would shift every row after it on every
|
|
1865
|
-
* delta (the parallel-tools jitter). */
|
|
1866
|
-
function liveWindow(text, W) {
|
|
1867
|
-
const p = palette();
|
|
1868
|
-
if (text === "") {
|
|
1869
|
-
// R7a: blank, not two bare gutters. A `│` marks a row that HAS
|
|
1870
|
-
// content; two of them above "waiting for output" drew a tall
|
|
1871
|
-
// empty bar under every command that had not printed yet — which
|
|
1872
|
-
// is most of them, for their first second.
|
|
1873
|
-
// ...and the waiting row sits DIRECTLY under its header, with the
|
|
1874
|
-
// blanks below it — VD-4's own rule ("the output starts under its
|
|
1875
|
-
// own header and grows downward"), which the gutter rows used to
|
|
1876
|
-
// satisfy by accident and blanks made visible as a two-row gap.
|
|
1877
|
-
return [`${p.dim}${NOTE_ROW}waiting for output${p.reset}`, "", ""];
|
|
1878
|
-
}
|
|
1879
|
-
const rows = blockRows(text, W);
|
|
1880
|
-
if (rows.length <= CAP_LIVE_WINDOW) {
|
|
1881
|
-
while (rows.length < CAP_LIVE_WINDOW)
|
|
1882
|
-
rows.push(""); // R7a: blank, not a bar
|
|
1883
|
-
return rows;
|
|
1884
|
-
}
|
|
1885
|
-
const cut = foldLine(`${p.dim}${NOTE_ROW}+${rows.length - (CAP_LIVE_WINDOW - 1)} earlier rows · ctrl+o${p.reset}`, W);
|
|
1886
|
-
return [...rows.slice(rows.length - (CAP_LIVE_WINDOW - 1)), ...cut];
|
|
1760
|
+
// R13 — a failure is a CARD like any other call: the same cap of
|
|
1761
|
+
// five, the same note wording, the same direction (an error's answer
|
|
1762
|
+
// is at the top). It kept a cap of three and `+2 more · ctrl+o` from
|
|
1763
|
+
// before the card existed, which made the one shape a reader most
|
|
1764
|
+
// wants to read the one shape that showed least of itself.
|
|
1765
|
+
return previewHead(c.resultText.split("\n").slice(skipFirst).join("\n"), W, slabPaints() ? "body" : "dim");
|
|
1887
1766
|
}
|
|
1888
1767
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
1894
|
-
*
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1768
|
+
* DC-46 — THE RUNNING WINDOW GROWS, and nothing pads it.
|
|
1769
|
+
*
|
|
1770
|
+
* DECLARED REVERSAL of W8's fixed window and of E2 as first written
|
|
1771
|
+
* ("allocated at the settled card's height, and only ever shrinks at
|
|
1772
|
+
* settle"). Both fixed a height so it would not move while a command
|
|
1773
|
+
* ran; both fixed it at a height the SETTLE then changed, and the settle
|
|
1774
|
+
* is where the cost landed. A card allocated at twelve rows and settling
|
|
1775
|
+
* at three gives nine rows back, the window's top is clamped and cannot
|
|
1776
|
+
* follow, and the difference is a blank band above the composer.
|
|
1777
|
+
* Measured on the a7 replay: hole-frames 8.9 / 13.5 / 3.8 percent at
|
|
1778
|
+
* 0.23.0 against 16.9 / 24.6 / 7.9 with the shrink. The only source is
|
|
1779
|
+
* the shrink, so the cure is to stop shrinking.
|
|
1780
|
+
*
|
|
1781
|
+
* So the window IS its content: one row while nothing has arrived, one
|
|
1782
|
+
* more per line to the cap, then the cut note above a scrolling tail.
|
|
1783
|
+
* R7a's "blank, not a bar" retires with the padding it governed — it was
|
|
1784
|
+
* about what to draw on rows a FIXED height reserved, and no height is
|
|
1785
|
+
* reserved now.
|
|
1786
|
+
*
|
|
1787
|
+
* The direction is the SETTLED card's, so a settle swaps content and
|
|
1788
|
+
* moves nothing: a shell shows its tail with the note above, everything
|
|
1789
|
+
* else its head with the note below.
|
|
1904
1790
|
*/
|
|
1905
|
-
function
|
|
1906
|
-
if (text === "")
|
|
1907
|
-
return liveWindow("", W);
|
|
1791
|
+
function liveWindow(c, W, cap, tone) {
|
|
1908
1792
|
const p = palette();
|
|
1909
|
-
// TUI2-R1.5 ④(b) (VD-4):
|
|
1910
|
-
//
|
|
1911
|
-
//
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
//
|
|
1915
|
-
//
|
|
1916
|
-
//
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1793
|
+
// TUI2-R1.5 ④(b) (VD-4): leading empty lines in the sidecar (a
|
|
1794
|
+
// 4096-byte tail can begin on a line boundary) are skipped, so the
|
|
1795
|
+
// output starts under its own header.
|
|
1796
|
+
const all = blockRows(c.resultText, W, tone);
|
|
1797
|
+
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1798
|
+
// DC-46, derived — NOTHING YET IS NO WINDOW AT ALL, so a running call
|
|
1799
|
+
// with no output is the same THREE-ROW card as a settled one with
|
|
1800
|
+
// none. The ruling's skeleton put a `waiting for output` row here; a
|
|
1801
|
+
// command that returns nothing (`true`, a silent build) would then
|
|
1802
|
+
// settle from seven rows to three, which is the very shrink the ruling
|
|
1803
|
+
// exists to remove — its own rule cannot hold with that row in place.
|
|
1804
|
+
//
|
|
1805
|
+
// Nothing is lost: the breathing mark says the call is in flight and
|
|
1806
|
+
// the status row's elapsed says how long, so a row reading "waiting
|
|
1807
|
+
// for output" carries no fact they do not (§1.3). The card grows the
|
|
1808
|
+
// instant a line arrives, and growth is what this design permits.
|
|
1809
|
+
if (from < 0)
|
|
1810
|
+
return [];
|
|
1811
|
+
const rows = all.slice(from);
|
|
1812
|
+
if (rows.length <= cap)
|
|
1813
|
+
return rows;
|
|
1814
|
+
return c.name === "shell"
|
|
1815
|
+
? [...cutNote(rows.length - cap, "earlier", W, tone), ...rows.slice(rows.length - cap)]
|
|
1816
|
+
: [...rows.slice(0, cap), ...cutNote(rows.length - cap, "more", W, tone)];
|
|
1817
|
+
}
|
|
1818
|
+
/** DC-46 — the window's HIGH-WATER, per cell: the room a frame leaves
|
|
1819
|
+
* caps how far a window may GROW, and never shrinks one that already
|
|
1820
|
+
* grew. Without this a second call starting would pull the first's
|
|
1821
|
+
* window in, which is the same shrink by another route. Keyed on the
|
|
1822
|
+
* cell, like `blockMemo`, and it only ever matters while the cell is
|
|
1823
|
+
* live — a settled cell renders from its result alone. */
|
|
1824
|
+
const liveHighWater = new WeakMap();
|
|
1825
|
+
function liveCap(c, room) {
|
|
1826
|
+
const want = Math.min(CAP_PREVIEW, Math.max(1, room));
|
|
1827
|
+
const held = liveHighWater.get(c) ?? 0;
|
|
1828
|
+
const cap = Math.max(want, held);
|
|
1829
|
+
liveHighWater.set(c, cap);
|
|
1830
|
+
return cap;
|
|
1930
1831
|
}
|
|
1931
1832
|
/**
|
|
1932
1833
|
* R4 — the standing act slot.
|
|
@@ -1972,7 +1873,7 @@ export function slotTail(text, W, rows) {
|
|
|
1972
1873
|
return [];
|
|
1973
1874
|
const p = palette();
|
|
1974
1875
|
const all = blockRows(text, W);
|
|
1975
|
-
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(
|
|
1876
|
+
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1976
1877
|
const body = from < 0 ? [] : all.slice(from);
|
|
1977
1878
|
// R7a: no pad. The slot stopped padding (see slotPad) and this was
|
|
1978
1879
|
// the same pad by another route — three blank rows under a call with
|
|
@@ -2133,7 +2034,18 @@ class MarkdownBlock {
|
|
|
2133
2034
|
this.cell = cell;
|
|
2134
2035
|
}
|
|
2135
2036
|
render(W, _ctx) {
|
|
2136
|
-
|
|
2037
|
+
// R13 E3 — THE MODEL'S WORDS MOVE TO COLUMN 2. A card's rows sit
|
|
2038
|
+
// there (E4) and the chip's text does too (D4), so prose at column
|
|
2039
|
+
// 0 would leave the page with three left edges for three registers
|
|
2040
|
+
// — the opposite of one rhythm. The registers are told apart by
|
|
2041
|
+
// SURFACE, which is §1.6's argument; the column is not one of the
|
|
2042
|
+
// things doing that work.
|
|
2043
|
+
//
|
|
2044
|
+
// The block folds in the room the indent leaves, so invariant ①
|
|
2045
|
+
// holds by construction. A block's own leading blank (its `gap`)
|
|
2046
|
+
// stays EMPTY: an indented blank row is trailing whitespace, and
|
|
2047
|
+
// §1.3 forbids a mark on a row with nothing to mark.
|
|
2048
|
+
return renderBlock(this.cell.block, Math.max(1, W - PROSE_COL.length)).map((r) => (r === "" ? r : `${PROSE_COL}${r}`));
|
|
2137
2049
|
}
|
|
2138
2050
|
}
|
|
2139
2051
|
/** The notice lines — the error surface. */
|