@vincemakes/kiso-tui-cells 0.22.0 → 0.24.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 +18 -67
- package/dist/components.js +375 -516
- package/dist/ground.d.ts +16 -2
- package/dist/ground.js +18 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.d.ts +13 -2
- package/dist/render.js +18 -1
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -118,9 +118,20 @@ export { visibleWidth } from "./width.js";
|
|
|
118
118
|
export function bodySpacing(prev, rows) {
|
|
119
119
|
if (rows.length === 0 || prev === null || prev.length === 0)
|
|
120
120
|
return rows;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// R13 D1 — ONE blank between any two elements, whatever their height.
|
|
122
|
+
//
|
|
123
|
+
// W11 spaced by height: one-row siblings packed tight, anything
|
|
124
|
+
// multi-row breathed on both sides. A reader could not tell where the
|
|
125
|
+
// next blank would fall — and worse, the spacing was a function of a
|
|
126
|
+
// cell's CURRENT height, so a cell growing from one row to five moved
|
|
127
|
+
// everything around it. That is the mechanism behind R7a and behind
|
|
128
|
+
// R12 Round 2's settle shift, both of them "the screen moved under
|
|
129
|
+
// the reader".
|
|
130
|
+
//
|
|
131
|
+
// A constant cannot do that: a live block, its settled form and the
|
|
132
|
+
// card it becomes are spaced identically BY CONSTRUCTION, which is
|
|
133
|
+
// exactly what R7a's one-row stand-in was simulating.
|
|
134
|
+
return ["", ...rows];
|
|
124
135
|
}
|
|
125
136
|
/** The container — vertical concatenation with the W11 formula. No
|
|
126
137
|
* component decides its own spacing: every blank in the body is the
|
|
@@ -212,6 +223,9 @@ export function cellComponent(cell) {
|
|
|
212
223
|
* the same rule as three thousand short ones.
|
|
213
224
|
*/
|
|
214
225
|
const USER_CHIP_ROWS = 12;
|
|
226
|
+
/** R13 D4 — the chip's inner pad: two columns, so its text begins in
|
|
227
|
+
* the same column as everything else on the page. */
|
|
228
|
+
const CHIP_PAD = " ";
|
|
215
229
|
class UserMessage {
|
|
216
230
|
cell;
|
|
217
231
|
constructor(cell) {
|
|
@@ -219,7 +233,12 @@ class UserMessage {
|
|
|
219
233
|
}
|
|
220
234
|
render(W, _ctx) {
|
|
221
235
|
const p = palette();
|
|
222
|
-
|
|
236
|
+
// R13 D4 — TWO columns of inner pad, where R2 had one. The chip
|
|
237
|
+
// keeps its surface (reverse video, one row per folded line, no
|
|
238
|
+
// pad rows — R12 Round 2's ruling stands); what changes is where
|
|
239
|
+
// its text STARTS, so the human's words begin in the same column
|
|
240
|
+
// as the model's (E3) and as a card's rows (E4).
|
|
241
|
+
const chipW = Math.max(1, W - 2 * CHIP_PAD.length);
|
|
223
242
|
const rows = [];
|
|
224
243
|
// REL-0152-D13: fold only as far as the bound needs. A pasted file
|
|
225
244
|
// has thousands of lines and folding all of them to show twelve is
|
|
@@ -264,7 +283,7 @@ class UserMessage {
|
|
|
264
283
|
// row is two cells per character and pads by cells.
|
|
265
284
|
const inner = chipW;
|
|
266
285
|
for (const row of content) {
|
|
267
|
-
rows.push(`${p.rv}
|
|
286
|
+
rows.push(`${p.rv}${CHIP_PAD}${row}${" ".repeat(Math.max(0, inner - displayWidth(row)))}${CHIP_PAD}${p.rvEnd}`);
|
|
268
287
|
}
|
|
269
288
|
if (!truncated)
|
|
270
289
|
return rows;
|
|
@@ -276,7 +295,16 @@ class UserMessage {
|
|
|
276
295
|
const shown = rows.length;
|
|
277
296
|
const total = paras.length;
|
|
278
297
|
const more = Math.max(0, total - shown);
|
|
279
|
-
|
|
298
|
+
// DC-45: THE NOTICE FOLDS TOO. It was written at a fixed 30 columns
|
|
299
|
+
// and emitted verbatim at every width, so a paste of thirteen lines
|
|
300
|
+
// in a terminal narrower than the sentence tripped the compositor's
|
|
301
|
+
// invariant ① and killed the session. The tiers are TUI2-R1.5 ⑤'s
|
|
302
|
+
// discipline: `sent in full` is the SEMANTICS — the whole reason
|
|
303
|
+
// the row exists — so the count gives way before it, and `cutLine`
|
|
304
|
+
// is the backstop that holds at any width there is.
|
|
305
|
+
const count = more > 0 ? `+${more} more line${more === 1 ? "" : "s"}` : "cut here";
|
|
306
|
+
const short = more > 0 ? `+${more}` : "cut";
|
|
307
|
+
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
308
|
return rows;
|
|
281
309
|
}
|
|
282
310
|
}
|
|
@@ -338,7 +366,27 @@ class ThinkingBlock {
|
|
|
338
366
|
const text = escapeTerminal(this.cell.text).trim();
|
|
339
367
|
if (text === "")
|
|
340
368
|
return [];
|
|
341
|
-
|
|
369
|
+
// DC-47 — THINKING GOES ONE LEVEL DEEPER THAN PROSE, and the
|
|
370
|
+
// reason is a law rather than a taste.
|
|
371
|
+
//
|
|
372
|
+
// §7.2: the indent is the price of §1.2 — italic and dim are
|
|
373
|
+
// escape sequences, so a rendered frame with its colour stripped
|
|
374
|
+
// (a terminal capture, a paste out of the scrollback, a log of
|
|
375
|
+
// what was drawn) would lose the line between the model's
|
|
376
|
+
// reasoning and its answer. R13's E3 moved PROSE to column 2,
|
|
377
|
+
// which is where the thinking already was, so after
|
|
378
|
+
// `sed 's/\x1b\[[0-9;]*m//g'` the two became the same row.
|
|
379
|
+
// Measured, not reasoned: both rendered
|
|
380
|
+
// `" Weighing the two shapes."` exactly.
|
|
381
|
+
//
|
|
382
|
+
// NOT a pipe, though §7.2 used to say so: `thinkingEnd`'s inactive
|
|
383
|
+
// path writes `foldThinking` — one dim line — so a pipe never sees
|
|
384
|
+
// a thinking paragraph to confuse with prose.
|
|
385
|
+
//
|
|
386
|
+
// So the thinking takes the next column in. It is still the only
|
|
387
|
+
// carrier that survives a pipe, and it is still one indent step —
|
|
388
|
+
// what moved is which step, because prose took the one it had.
|
|
389
|
+
const room = Math.max(1, W - THINK_COL.length);
|
|
342
390
|
const rows = [];
|
|
343
391
|
for (const para of text.split(/\n\s*\n/)) {
|
|
344
392
|
const flat = para.replace(/\s+/g, " ").trim();
|
|
@@ -350,7 +398,7 @@ class ThinkingBlock {
|
|
|
350
398
|
// invariant ①b (a row is one physical row) holds by
|
|
351
399
|
// construction rather than by remembering to split.
|
|
352
400
|
for (const line of foldLine(flat, room))
|
|
353
|
-
rows.push(
|
|
401
|
+
rows.push(`${THINK_COL}${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
|
|
354
402
|
}
|
|
355
403
|
return rows;
|
|
356
404
|
}
|
|
@@ -633,39 +681,10 @@ class ToolExecution {
|
|
|
633
681
|
const c = this.cell;
|
|
634
682
|
const verb = escapeTerminal(displayVerb(c.name));
|
|
635
683
|
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
|
-
}
|
|
684
|
+
// R13 — the W13 rollup row and TUI2-R1 (B)'s exploration row stood
|
|
685
|
+
// here, ahead of everything else a settled call could be. Both are
|
|
686
|
+
// retired with the `rolled` field they read (see the compositor's
|
|
687
|
+
// #foldOrRollup for the reversal in full).
|
|
669
688
|
if (c.state === "done") {
|
|
670
689
|
// R3i phase 5: an answered (or declined) ask_user renders its
|
|
671
690
|
// OWN block — the questions and what the human said. The row
|
|
@@ -689,7 +708,7 @@ class ToolExecution {
|
|
|
689
708
|
if (c.reason !== null) {
|
|
690
709
|
const by = attribution(c);
|
|
691
710
|
const out = gutterCut(" ", `${p.red}${escapeTerminal(`${c.name} ${toolTargetOf(c)}`)} (${escapeTerminal(c.reason)}${by})${p.reset}`, W);
|
|
692
|
-
out.push(...toolBlockBody(c, W));
|
|
711
|
+
out.push(...toolBlockBody(c, W, ctx));
|
|
693
712
|
return out;
|
|
694
713
|
}
|
|
695
714
|
const elapsed = c.startedAt !== null && c.doneAt !== null ? ((c.doneAt - c.startedAt) / 1000).toFixed(1) : "?";
|
|
@@ -703,8 +722,17 @@ class ToolExecution {
|
|
|
703
722
|
// "200 of 250 lines", a diff's "+1 -1", a shell's "exit 0" — is a
|
|
704
723
|
// different fact and stays.
|
|
705
724
|
const rawMeta = settledMeta(c);
|
|
706
|
-
|
|
707
|
-
|
|
725
|
+
// VD-6's suppression RETIRED with the thing it was suppressing.
|
|
726
|
+
// A read card said `(2 lines, 0.0s) · 2 lines · ctrl+o expands`
|
|
727
|
+
// because the parens and the suffix were written by different
|
|
728
|
+
// rounds, each unaware the other was counting; the fix blanked
|
|
729
|
+
// the meta whenever the suffix would repeat it. R13 takes the
|
|
730
|
+
// count OUT of the suffix, so there is one place again and the
|
|
731
|
+
// meta is it — blanking it now would drop the fact entirely.
|
|
732
|
+
// "Stated exactly once" is unchanged and is what `countedHead`
|
|
733
|
+
// below preserves: a meta that already counts lines gets no
|
|
734
|
+
// second count beside it.
|
|
735
|
+
const meta = escapeTerminal(rawMeta);
|
|
708
736
|
// A4: the target rides the settled head row — the verb's
|
|
709
737
|
// summary column (W3's 5-char pad keeps the paths lined up).
|
|
710
738
|
// A5: an extension's auto-approval appends `· approved by
|
|
@@ -724,7 +752,9 @@ class ToolExecution {
|
|
|
724
752
|
// TUI2-R1.5 ⑤: the shortest tier is RESERVED — the affordance is
|
|
725
753
|
// the semantics. TUI2-R1.5 pin 4: and the parts give way in a
|
|
726
754
|
// PINNED ORDER, rather than whichever happened to be last.
|
|
727
|
-
const
|
|
755
|
+
const nAll = countLines(c.resultText);
|
|
756
|
+
const countedHead = nAll > 0 && !/^\d+( of \d+)? lines?$/.test(rawMeta) ? `${nAll} line${nAll === 1 ? "" : "s"}` : "";
|
|
757
|
+
const text = settledHeadText(verbCol, escapeTerminal(toolTargetOf(c)), meta, approvedBy, elapsed, W - 2 - (hidden === null ? 0 : SUFFIX_MIN), countedHead);
|
|
728
758
|
// R2 (owner, 2026-08-27): no tick, no cross. A symbol earns its
|
|
729
759
|
// cell by carrying a fact the words do not, and a row that
|
|
730
760
|
// already says `exit 0` does not need one more thing saying it
|
|
@@ -732,8 +762,7 @@ class ToolExecution {
|
|
|
732
762
|
// metadata, in words, which is also the only form that survives
|
|
733
763
|
// a pipe with the colour stripped. A failure keeps its colour
|
|
734
764
|
// AND its words — see settledMeta.
|
|
735
|
-
const
|
|
736
|
-
const body = parts.rows;
|
|
765
|
+
const body = toolBlockParts(c, W, ctx).rows;
|
|
737
766
|
if (body.length > 0 && c.expanded) {
|
|
738
767
|
// An EXPANDED block is already showing everything, and its own
|
|
739
768
|
// footer ("ctrl+o collapses") is what closes it. Giving it an
|
|
@@ -743,13 +772,13 @@ class ToolExecution {
|
|
|
743
772
|
const head = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
744
773
|
return slabBlock(appendSuffix(head, expandSuffix(hidden, W - visibleWidth(head))), body, null, W);
|
|
745
774
|
}
|
|
746
|
-
if (
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
775
|
+
if (body.length > 0) {
|
|
776
|
+
// R13 — THE CARD, when there is something to preview: the head
|
|
777
|
+
// row names the call, the preview sits inside, and the outcome
|
|
778
|
+
// CLOSES it on its own line (§7.5's words, moved off the head
|
|
779
|
+
// row because the head row is no longer the only row). D6: the
|
|
780
|
+
// target is bold there — the head row's job is to say WHAT was
|
|
781
|
+
// run, and the metadata has its own row now.
|
|
753
782
|
//
|
|
754
783
|
// A failure takes NO tint on the head row (R9): only the
|
|
755
784
|
// outcome word is coloured, which is §1.2 exactly — the
|
|
@@ -767,28 +796,28 @@ class ToolExecution {
|
|
|
767
796
|
// what happened and how long it took — is never cut open.
|
|
768
797
|
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
769
798
|
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(
|
|
799
|
+
const words = pickTier([join(meta, counted, `${elapsed}s`, attr), join(meta, counted, `${elapsed}s`), join(meta, `${elapsed}s`), meta], W - visibleWidth(noteIndent()));
|
|
771
800
|
const outcome = c.isError ? `${p.red}${words}${p.reset}` : words;
|
|
772
801
|
return slabBlock(head, body, outcome, W);
|
|
773
802
|
}
|
|
774
|
-
//
|
|
775
|
-
//
|
|
803
|
+
// R13 — nothing to preview: the SAME card, three rows, with the
|
|
804
|
+
// outcome riding the head row because there is nothing between
|
|
805
|
+
// them to close.
|
|
776
806
|
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
return out;
|
|
807
|
+
// DECLARED REVERSAL of the owner's 2026-09-02 narrowing ("a call
|
|
808
|
+
// with no output on screen has no slab at all"), which read §1.6
|
|
809
|
+
// as giving the wash to the machine's VERBATIM text only. The
|
|
810
|
+
// ruling of 2026-09-03 supersedes it: the surface says WORK, not
|
|
811
|
+
// VERBATIM, and a page where some calls are cards and others are
|
|
812
|
+
// loose rows is the instability the owner was pointing at. §1.6
|
|
813
|
+
// moves with it.
|
|
814
|
+
const bare = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
815
|
+
return slabBlock(appendSuffix(bare, expandSuffix(hidden, W - visibleWidth(bare))), [], null, W);
|
|
787
816
|
}
|
|
788
817
|
if (c.state === "approval") {
|
|
789
818
|
// W2: the ❯ is the GUTTER (the left edge), never the line's tail
|
|
790
819
|
const out = gutterCut(`${p.bold}❯${p.reset} `, `${verbCol} ${liveTarget(c)}`, W);
|
|
791
|
-
out.push(...toolBlockBody(c, W));
|
|
820
|
+
out.push(...toolBlockBody(c, W, ctx));
|
|
792
821
|
return out;
|
|
793
822
|
}
|
|
794
823
|
if (c.state === "running") {
|
|
@@ -809,10 +838,44 @@ class ToolExecution {
|
|
|
809
838
|
// cannot be predicted: a turning mark implies progress the
|
|
810
839
|
// product does not have. With no ground the breath freezes to a
|
|
811
840
|
// static `●` and says the same thing more quietly.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
841
|
+
// R13 E2 — A RUNNING CALL IS THE SAME CARD, allocated at the
|
|
842
|
+
// SETTLED card's height from its first frame. The settle then
|
|
843
|
+
// changes CONTENT and never position: the spinner becomes two
|
|
844
|
+
// spaces in the same two columns, the live window gives back
|
|
845
|
+
// the rows the result did not need, and the metadata row that
|
|
846
|
+
// said `3s` says `exit 0 · 90 lines · 3.2s`.
|
|
847
|
+
//
|
|
848
|
+
// The elapsed moves OFF the head row onto that metadata row,
|
|
849
|
+
// which is where the settled card keeps it — the head row's job
|
|
850
|
+
// is to say what is running, and it says the same thing before
|
|
851
|
+
// and after.
|
|
852
|
+
//
|
|
853
|
+
// DC-43: with too little room for the seven-row skeleton (two
|
|
854
|
+
// pads, the head, two blanks, one preview row and the metadata)
|
|
855
|
+
// there is no card — the call keeps its head row until it
|
|
856
|
+
// commits, which is the one form that fits anywhere.
|
|
857
|
+
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
858
|
+
const gutter = ctx.grouped === true ? " " : `${breathFrame(ctx.spinnerI)} `;
|
|
859
|
+
if (liveRows <= 0) {
|
|
860
|
+
// the degraded form is the head row ALONE, so it keeps the
|
|
861
|
+
// duration it would otherwise have lost with its card — cut
|
|
862
|
+
// against the room the duration leaves (VD-4: the duration
|
|
863
|
+
// is its own segment, never welded to a cut word).
|
|
864
|
+
const bare = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
|
|
865
|
+
return [`${bare[0]}${p.dim}${dur}${p.reset}`];
|
|
866
|
+
}
|
|
867
|
+
const head = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, W)[0];
|
|
868
|
+
// DC-46 — the two GESTURES ride the status row, where they cost
|
|
869
|
+
// nothing. They were a footer INSIDE the window, spending one of
|
|
870
|
+
// its rows on a sentence that is not output; with the window
|
|
871
|
+
// grown from its content, that row was the difference between a
|
|
872
|
+
// settle that swaps content and one that changes height. The
|
|
873
|
+
// row's shape is the settled outcome row's, so the settle
|
|
874
|
+
// rewrites it in place: `3s · esc stops` → `exit 0 · 90 lines ·
|
|
875
|
+
// 3.2s`.
|
|
876
|
+
const gestures = c.name === "shell" ? " · esc stops · alt+⏎ redirects" : "";
|
|
877
|
+
const status = pickTier([`${elapsed}s${gestures}`, `${elapsed}s`], Math.max(1, W - visibleWidth(noteIndent())));
|
|
878
|
+
return slabBlock(head, toolBlockBody(c, W, ctx), status, W);
|
|
816
879
|
}
|
|
817
880
|
// W2: ◦ replaces → for QUEUED — · is the separator inside every
|
|
818
881
|
// metadata group; a queued marker that is also the separator
|
|
@@ -839,7 +902,7 @@ class ToolExecution {
|
|
|
839
902
|
* never a cap.
|
|
840
903
|
*/
|
|
841
904
|
function hiddenLines(c, W) {
|
|
842
|
-
if (c.expanded || c.state !== "done" || c.
|
|
905
|
+
if (c.expanded || c.state !== "done" || c.reason !== null)
|
|
843
906
|
return null;
|
|
844
907
|
if (c.name === "delegate")
|
|
845
908
|
return null; // its body is the one-line summary, always whole
|
|
@@ -848,13 +911,14 @@ function hiddenLines(c, W) {
|
|
|
848
911
|
return null;
|
|
849
912
|
if (c.isError)
|
|
850
913
|
return null; // errorBody's own cut row is the affordance there
|
|
851
|
-
//
|
|
852
|
-
//
|
|
914
|
+
// R13: a call that PREVIEWS carries the key on its own note row when
|
|
915
|
+
// something is cut, and needs no affordance at all when nothing is.
|
|
853
916
|
// 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
|
-
|
|
917
|
+
// one cell — the thing that rule exists to forbid. read_file is the
|
|
918
|
+
// one call with no preview (E1), so the key lives on its head row.
|
|
919
|
+
if (c.name !== "read_file")
|
|
856
920
|
return null;
|
|
857
|
-
return n;
|
|
921
|
+
return n;
|
|
858
922
|
}
|
|
859
923
|
/**
|
|
860
924
|
* TUI2-R1 (A) — the suffix, in the width that is LEFT.
|
|
@@ -900,12 +964,24 @@ const SUFFIX_MIN = " · ctrl+o".length;
|
|
|
900
964
|
* compressible thing on the row — a reader recognises a command
|
|
901
965
|
* from its head — and it is the only part with a natural ellipsis.
|
|
902
966
|
*/
|
|
903
|
-
function settledHeadText(verbCol, target, meta, attr, elapsed, room) {
|
|
904
|
-
|
|
905
|
-
|
|
967
|
+
function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "") {
|
|
968
|
+
// R13 — ONE GRAMMAR FOR BOTH CARDS. This row used to close with W4's
|
|
969
|
+
// parentheses — `read a.ts (0.1s) · 10 lines · ctrl+o expands` —
|
|
970
|
+
// while the bodied card's outcome row said `exit 0 · 90 lines · 0.4s`
|
|
971
|
+
// in a `·` chain. Two shapes for the same facts, and which one a call
|
|
972
|
+
// got depended on whether it happened to have a preview. The chain
|
|
973
|
+
// wins: it is the one the outcome row already uses, it reads in one
|
|
974
|
+
// direction, and it puts the elapsed where the other card puts it.
|
|
975
|
+
//
|
|
976
|
+
// The giving-way order is pin 4's, unchanged: the ATTRIBUTION drops
|
|
977
|
+
// first, then the count, and the core — what happened and how long it
|
|
978
|
+
// took — is never cut open; below that the target itself truncates.
|
|
979
|
+
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
980
|
+
const core = join(meta, counted, `${elapsed}s`);
|
|
981
|
+
const withAttr = join(meta, counted, `${elapsed}s`, attr.replace(" · ", ""));
|
|
906
982
|
const lead = `${verbCol} `;
|
|
907
|
-
const fit = (t,
|
|
908
|
-
const line = `${lead}${t}${
|
|
983
|
+
const fit = (t, tail) => {
|
|
984
|
+
const line = `${lead}${t}${tail === "" ? "" : ` · ${tail}`}`;
|
|
909
985
|
return visibleWidth(line) <= room ? line : null;
|
|
910
986
|
};
|
|
911
987
|
// 1. everything
|
|
@@ -916,10 +992,16 @@ function settledHeadText(verbCol, target, meta, attr, elapsed, room) {
|
|
|
916
992
|
const bare = fit(target, core);
|
|
917
993
|
if (bare !== null)
|
|
918
994
|
return bare;
|
|
995
|
+
// 2b. the COUNT gives way next (pin 4), where the suffix is not
|
|
996
|
+
// already carrying it
|
|
997
|
+
const short = fit(target, join(meta, `${elapsed}s`));
|
|
998
|
+
if (short !== null)
|
|
999
|
+
return short;
|
|
919
1000
|
// 3. the target truncates, the core stays whole
|
|
920
|
-
const
|
|
1001
|
+
const stem = join(meta, `${elapsed}s`);
|
|
1002
|
+
const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
|
|
921
1003
|
if (budget >= 1)
|
|
922
|
-
return `${lead}${widthCut(target, budget)}… ${
|
|
1004
|
+
return `${lead}${widthCut(target, budget)}… · ${stem}`;
|
|
923
1005
|
// 4. below that even the core cannot ride: the row is the call's
|
|
924
1006
|
// identity and its affordance, and no half-open parenthesis.
|
|
925
1007
|
return `${lead}${widthCut(target, Math.max(1, room - visibleWidth(lead)))}`;
|
|
@@ -947,8 +1029,13 @@ function attribution(c) {
|
|
|
947
1029
|
export function expandSuffix(lines, room) {
|
|
948
1030
|
if (lines === null)
|
|
949
1031
|
return "";
|
|
950
|
-
|
|
951
|
-
|
|
1032
|
+
// R13 — the COUNT left this suffix for the head row's own `·` chain,
|
|
1033
|
+
// where the bodied card keeps it too (`… · 10 lines · 0.1s`). It was
|
|
1034
|
+
// here because the parenthesised core had nowhere to put it and the
|
|
1035
|
+
// suffix was the only tail the row had; with one grammar for both
|
|
1036
|
+
// cards there is one place, and VD-6's "stated exactly once" is what
|
|
1037
|
+
// forbids leaving a copy behind.
|
|
1038
|
+
for (const tier of [" · ctrl+o expands", " · ctrl+o"]) {
|
|
952
1039
|
if (tier.length <= room)
|
|
953
1040
|
return tier;
|
|
954
1041
|
}
|
|
@@ -1051,58 +1138,6 @@ const EXPLORE_NOUN = {
|
|
|
1051
1138
|
export function isExploreTool(name) {
|
|
1052
1139
|
return EXPLORE_NOUN[name] !== undefined;
|
|
1053
1140
|
}
|
|
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
1141
|
/** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
|
|
1107
1142
|
* scrollback, becomes ONE line — the work order's claimed shape
|
|
1108
1143
|
* (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
|
|
@@ -1340,252 +1375,19 @@ function stretchTerms(t, live) {
|
|
|
1340
1375
|
return `${e[tense(name)]} ${n} ${n === 1 ? e[2] : e[3]}`;
|
|
1341
1376
|
});
|
|
1342
1377
|
}
|
|
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
1378
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1583
1379
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
1584
1380
|
* (the W7 table). The renderer-cut row is inside the cap. */
|
|
1585
|
-
|
|
1586
|
-
|
|
1381
|
+
/** R13 — ONE preview cap, every tool. It was the shell's alone while
|
|
1382
|
+
* the shell was the only settled call with rows on screen. */
|
|
1383
|
+
export const CAP_PREVIEW = 5;
|
|
1384
|
+
/** DC-46 — the running window's ceiling is the SETTLED preview's, and a
|
|
1385
|
+
* running card reaches it by growing rather than by being handed it.
|
|
1386
|
+
* `LIVE_WINDOW` (CAP_PREVIEW + 1) retires with the allocation it sized. */
|
|
1387
|
+
/** The rows a card costs besides its window: two pads, the head, two
|
|
1388
|
+
* blanks and the status row. Below this there is no card (DC-43). */
|
|
1389
|
+
export const CARD_CHROME = 6;
|
|
1587
1390
|
const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
1588
|
-
const CAP_ERROR = 3; // the error text head
|
|
1589
1391
|
/** The block body rows' prefixes (W2's gutter table): │ a bounded
|
|
1590
1392
|
* block's body, └ the block's last row — what was cut, where the rest
|
|
1591
1393
|
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
@@ -1600,13 +1402,27 @@ const CAP_ERROR = 3; // the error text head
|
|
|
1600
1402
|
* prose (2). Bytes still tell them apart; no column of glyphs.
|
|
1601
1403
|
*
|
|
1602
1404
|
* `└` survives as the mark that OPENS the block, once, on its first
|
|
1603
|
-
* row (see openBlock). In-block notes take
|
|
1405
|
+
* row (see openBlock). In-block notes take the same indent,
|
|
1604
1406
|
* no glyph — because a second `└` inside one block would be the same
|
|
1605
1407
|
* mark meaning two things (§4.1). CUT_ROW is unchanged for the
|
|
1606
1408
|
* surfaces that are not a tool block: the fold row's target list, the
|
|
1607
1409
|
* slot's overflow count. */
|
|
1608
|
-
|
|
1609
|
-
|
|
1410
|
+
/** R8a's four columns — off the surface. Inside a painted card every
|
|
1411
|
+
* row sits at column 2 (R13 E4): the head row and the outcome row
|
|
1412
|
+
* bracket the preview, so the indent is no longer what says "these
|
|
1413
|
+
* rows are output". Where nothing paints, it is exactly that, and R8a
|
|
1414
|
+
* stands unchanged. */
|
|
1415
|
+
const BODY_ROW_FLAT = " ";
|
|
1416
|
+
const NOTE_ROW_FLAT = " ";
|
|
1417
|
+
const CARD_ROW = " ";
|
|
1418
|
+
/** R13 E3 — the column the model's words begin in, the same one the
|
|
1419
|
+
* card's rows and the chip's text begin in. */
|
|
1420
|
+
const PROSE_COL = " ";
|
|
1421
|
+
/** DC-47 — the model's THINKING, one level deeper than its prose, so
|
|
1422
|
+
* the two are still told apart once the escapes are stripped (§1.2). */
|
|
1423
|
+
const THINK_COL = " ";
|
|
1424
|
+
const bodyRow = () => (slabPaints() ? CARD_ROW : BODY_ROW_FLAT);
|
|
1425
|
+
const noteIndent = () => (slabPaints() ? CARD_ROW : NOTE_ROW_FLAT);
|
|
1610
1426
|
const CUT_ROW = "└ ";
|
|
1611
1427
|
/**
|
|
1612
1428
|
* R9 P2 — THE SLAB: a single call's block is one washed object.
|
|
@@ -1619,7 +1435,7 @@ const CUT_ROW = "└ ";
|
|
|
1619
1435
|
*
|
|
1620
1436
|
* THE DEGRADATION IS THE POINT OF THE PREDICATE. `wash` is a chosen
|
|
1621
1437
|
* background on the two KNOWN grounds and reverse video on the third
|
|
1622
|
-
* (§3 rung
|
|
1438
|
+
* (§3's last rung). A chip inverting for one row is the design working; eight
|
|
1623
1439
|
* output rows inverting is a blackboard in the middle of the transcript.
|
|
1624
1440
|
* So a slab paints only where the wash is a real background, and where
|
|
1625
1441
|
* it is not the block degrades to what it has always been — the R8a
|
|
@@ -1665,7 +1481,7 @@ function noteRow(text, W, tone) {
|
|
|
1665
1481
|
// its own. The row names a fact; a cut names it shorter.
|
|
1666
1482
|
const open = tone === "body" ? p.washDim : p.dim;
|
|
1667
1483
|
const close = tone === "body" ? p.washDimEnd : p.reset;
|
|
1668
|
-
return [cutLine(`${open}${
|
|
1484
|
+
return [cutLine(`${open}${noteIndent()}${text}${close}`, W)];
|
|
1669
1485
|
}
|
|
1670
1486
|
/**
|
|
1671
1487
|
* Assemble a call's block.
|
|
@@ -1682,18 +1498,24 @@ function slabBlock(head, body, outcome, W) {
|
|
|
1682
1498
|
out.push(...noteRow(outcome, W, "dim"));
|
|
1683
1499
|
return out;
|
|
1684
1500
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1501
|
+
// R13 — THE CARD. pad · head · blank · preview · blank · outcome ·
|
|
1502
|
+
// pad, and a call with nothing to preview is three rows with its
|
|
1503
|
+
// outcome riding the head. Every row sits at column 2 (E4): R8a put
|
|
1504
|
+
// a block's body at column 4 so a pipe could tell output from prose,
|
|
1505
|
+
// and inside a painted card the head and the outcome bracket it
|
|
1506
|
+
// instead — off the surface R8a's indent is still the fact, which is
|
|
1507
|
+
// why the degradation above keeps it.
|
|
1508
|
+
if (body.length === 0) {
|
|
1509
|
+
const only = outcome === null ? head : `${head} ${outcome}`;
|
|
1510
|
+
return [slabRow("", W), slabRow(only, W), slabRow("", W)];
|
|
1695
1511
|
}
|
|
1696
|
-
|
|
1512
|
+
const top = [slabRow("", W), slabRow(head, W), slabRow("", W), ...body.map((r) => slabRow(r, W))];
|
|
1513
|
+
// An EXPANDED block closes with its own footer and passes no outcome;
|
|
1514
|
+
// a blank row and an empty metadata row under it would be §1.3's
|
|
1515
|
+
// empty mark twice over.
|
|
1516
|
+
if (outcome === null)
|
|
1517
|
+
return [...top, slabRow("", W)];
|
|
1518
|
+
return [...top, slabRow("", W), ...noteRow(outcome, W, "body").map((r) => slabRow(r, W)), slabRow("", W)];
|
|
1697
1519
|
}
|
|
1698
1520
|
/** R8a — stamp `└` on a block's FIRST row, after every slice and note
|
|
1699
1521
|
* has been assembled, so the mark is always on the first row actually
|
|
@@ -1703,25 +1525,26 @@ function openBlock(rows) {
|
|
|
1703
1525
|
// or a blank leading output line can put an empty row first, and a
|
|
1704
1526
|
// corner there would be a mark on a row with nothing to mark — law
|
|
1705
1527
|
// 1.3, which is the rule this whole change is serving.
|
|
1706
|
-
const i = rows.findIndex((r) => visibleWidth(r) > visibleWidth(
|
|
1528
|
+
const i = rows.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1707
1529
|
if (i < 0)
|
|
1708
1530
|
return rows;
|
|
1709
1531
|
const first = rows[i];
|
|
1710
|
-
const at = first.indexOf(
|
|
1532
|
+
const at = first.indexOf(BODY_ROW_FLAT);
|
|
1711
1533
|
if (at < 0)
|
|
1712
1534
|
return rows;
|
|
1713
1535
|
// the corner REPLACES two of the four indent columns, so the text
|
|
1714
1536
|
// 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 +
|
|
1537
|
+
return [...rows.slice(0, i), `${first.slice(0, at)} \u2514 ${first.slice(at + BODY_ROW_FLAT.length)}`, ...rows.slice(i + 1)];
|
|
1716
1538
|
}
|
|
1717
1539
|
const blockMemo = new WeakMap();
|
|
1718
1540
|
/** The block's body rows below the header (memoized, W9). */
|
|
1719
|
-
function toolBlockParts(c, W) {
|
|
1541
|
+
function toolBlockParts(c, W, ctx) {
|
|
1720
1542
|
const memo = blockMemo.get(c);
|
|
1721
1543
|
// the SURFACE is part of the key: the same cell renders different rows
|
|
1722
1544
|
// painted and unpainted, and a ground resolved after the first frame
|
|
1723
1545
|
// would otherwise be served the pre-ground shape forever.
|
|
1724
|
-
const
|
|
1546
|
+
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
1547
|
+
const state = `${c.state}:${c.isError}:${c.name}:${c.expanded ? "x" : ""}:${slabPaints() ? "slab" : "flat"}:${liveRows}`;
|
|
1725
1548
|
const content = c.state === "approval" ? (c.diff ?? null) : c.resultText;
|
|
1726
1549
|
if (memo !== undefined && memo.width === W && memo.state === state && memo.content === content)
|
|
1727
1550
|
return memo;
|
|
@@ -1743,62 +1566,71 @@ function toolBlockParts(c, W) {
|
|
|
1743
1566
|
? errorBody(c, W)
|
|
1744
1567
|
: c.name === "delegate"
|
|
1745
1568
|
? delegateSettled(c, W)
|
|
1746
|
-
: //
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
//
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
: []
|
|
1569
|
+
: // R13 — EVERY settled call previews. The shell's tail is
|
|
1570
|
+
// VD-5's own cap and direction (the conclusion is at the
|
|
1571
|
+
// end); everything else shows its head, because that is
|
|
1572
|
+
// where its answer is. read_file is the single exception
|
|
1573
|
+
// (E1): its result is the file, kiso has nothing to add by
|
|
1574
|
+
// showing five lines of it, and the head row's key opens
|
|
1575
|
+
// the whole thing.
|
|
1576
|
+
noPreview(c)
|
|
1577
|
+
? []
|
|
1578
|
+
: c.name === "shell"
|
|
1579
|
+
? shellTail(c.resultText, W, slabPaints() ? "body" : "dim")
|
|
1580
|
+
: previewHead(c.resultText, W, slabPaints() ? "body" : "dim")
|
|
1759
1581
|
: c.state === "running"
|
|
1760
1582
|
? c.name === "delegate"
|
|
1761
1583
|
? delegateRunning(c, W)
|
|
1762
|
-
:
|
|
1763
|
-
|
|
1764
|
-
|
|
1584
|
+
: // R13 E1 — the call with no preview settled has none while
|
|
1585
|
+
// it runs either; its card is three rows the whole way.
|
|
1586
|
+
noPreview({ ...c, state: "done" })
|
|
1587
|
+
? []
|
|
1588
|
+
: liveWindow(c, W, liveCap(c, liveRows), slabPaints() ? "body" : "dim")
|
|
1765
1589
|
: c.state === "approval"
|
|
1766
1590
|
? diffBody(c.diff, W)
|
|
1767
1591
|
: [];
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
|
|
1592
|
+
// E1 governs the PREVIEW — five lines of a file kiso has nothing to
|
|
1593
|
+
// add to — not kiso's sentence about the result. `offset=201 for the
|
|
1594
|
+
// rest` is actionable and no other row says it (W10 pins exactly
|
|
1595
|
+
// that), so a read the TOOL capped takes one body row and a read it
|
|
1596
|
+
// did not takes none. That shape difference is information: the two
|
|
1597
|
+
// events are different, and the row is what says so.
|
|
1773
1598
|
const note = c.expanded ? null : toolCutNote(c.name, c.resultText);
|
|
1774
1599
|
if (note !== null)
|
|
1775
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1600
|
+
rows.push(...foldLine(`${p.dim}${noteIndent()}${note}${p.reset}`, W));
|
|
1776
1601
|
// TUI2-R1 (A): an EXPANDED block says how to put it back. The footer
|
|
1777
1602
|
// rides a block that HAS rows — an expanded delegate whose summary
|
|
1778
1603
|
// marker is missing renders nothing, and a lone footer under a head
|
|
1779
1604
|
// row would be an affordance for an empty block.
|
|
1780
1605
|
if (c.expanded && rows.length > 0)
|
|
1781
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1606
|
+
rows.push(...foldLine(`${p.dim}${noteIndent()}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1782
1607
|
// R9 P2: `└` opens a block that has no surface. Inside a slab the
|
|
1783
1608
|
// surface IS the container, and a corner in it is §1.3's empty mark
|
|
1784
1609
|
// one scale up — so the corner and the slab are alternatives, never
|
|
1785
1610
|
// both.
|
|
1786
1611
|
const opened = slabPaints() ? rows : openBlock(rows);
|
|
1787
|
-
const parts = { width: W, state, content, rows: opened
|
|
1612
|
+
const parts = { width: W, state, content, rows: opened };
|
|
1788
1613
|
blockMemo.set(c, parts);
|
|
1789
1614
|
return parts;
|
|
1790
1615
|
}
|
|
1791
|
-
/**
|
|
1792
|
-
*
|
|
1793
|
-
|
|
1794
|
-
|
|
1616
|
+
/** R13 E1 — the one settled call that previews NOTHING. A read's result
|
|
1617
|
+
* IS the file; five lines of it tell a reader less than the head row
|
|
1618
|
+
* already does, and the key opens the whole thing. (The reference
|
|
1619
|
+
* implementation makes the same call, for the same reason.) Everything
|
|
1620
|
+
* else previews: a shell its tail, the rest their head. */
|
|
1621
|
+
function noPreview(c) {
|
|
1622
|
+
return c.state === "done" && !c.expanded && !c.isError && c.reason === null && c.name === "read_file";
|
|
1795
1623
|
}
|
|
1796
|
-
/**
|
|
1624
|
+
/** The block's body rows. */
|
|
1625
|
+
function toolBlockBody(c, W, ctx) {
|
|
1626
|
+
return toolBlockParts(c, W, ctx).rows;
|
|
1627
|
+
}
|
|
1628
|
+
/** Fold result text into body rows (the block's own indent): escape,
|
|
1797
1629
|
* split, fold each line at W−prefix; trailing empty rows (the result's
|
|
1798
1630
|
* final newline) drop. */
|
|
1799
1631
|
function blockRows(text, W, tone = "dim") {
|
|
1800
1632
|
const p = palette();
|
|
1801
|
-
const textW = Math.max(1, W - visibleWidth(
|
|
1633
|
+
const textW = Math.max(1, W - visibleWidth(bodyRow()));
|
|
1802
1634
|
const rows = [];
|
|
1803
1635
|
// R9 P2: inside a SLAB the output rows are body strength, never dim —
|
|
1804
1636
|
// §2.1 bars dim from the wash (3.91:1 light, 4.35:1 dark) and these
|
|
@@ -1808,9 +1640,9 @@ function blockRows(text, W, tone = "dim") {
|
|
|
1808
1640
|
const close = tone === "dim" ? p.reset : "";
|
|
1809
1641
|
for (const raw of escapeTerminal(text).split("\n")) {
|
|
1810
1642
|
for (const row of foldLine(raw, textW))
|
|
1811
|
-
rows.push(`${open}${
|
|
1643
|
+
rows.push(`${open}${bodyRow()}${row}${close}`);
|
|
1812
1644
|
}
|
|
1813
|
-
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(
|
|
1645
|
+
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow()))
|
|
1814
1646
|
rows.pop();
|
|
1815
1647
|
return rows;
|
|
1816
1648
|
}
|
|
@@ -1820,23 +1652,43 @@ function blockRows(text, W, tone = "dim") {
|
|
|
1820
1652
|
* direction). */
|
|
1821
1653
|
function shellTail(text, W, tone = "dim") {
|
|
1822
1654
|
const rows = blockRows(text, W, tone);
|
|
1823
|
-
if (rows.length <=
|
|
1655
|
+
if (rows.length <= CAP_PREVIEW)
|
|
1824
1656
|
return rows;
|
|
1825
1657
|
// R9 P2 / D4: FIVE output rows, and the note is a row of its own. The
|
|
1826
1658
|
// pre-slab arithmetic spent one of the five on the cut note, because
|
|
1827
1659
|
// the note had nowhere else to live; the slab's metadata rows are not
|
|
1828
1660
|
// output and are not counted against the output's cap.
|
|
1829
|
-
const kept =
|
|
1661
|
+
const kept = CAP_PREVIEW;
|
|
1830
1662
|
// The note goes ABOVE the tail: it says what was cut, and what was
|
|
1831
1663
|
// cut is what came BEFORE these rows. One position on both surfaces —
|
|
1832
1664
|
// the surface degrades, the content shape does not.
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1665
|
+
return [...cutNote(rows.length - kept, "earlier", W, tone), ...rows.slice(rows.length - kept)];
|
|
1666
|
+
}
|
|
1667
|
+
/** R13 — the preview every OTHER settled call gets: the FIRST rows,
|
|
1668
|
+
* capped at five, the cut note BELOW them. A shell's conclusion is at
|
|
1669
|
+
* the bottom of its output, so its preview is the tail and its note
|
|
1670
|
+
* opens the block (shellTail above); a list, a search, a fetch all
|
|
1671
|
+
* answer at the top, so the note closes it. One rule, two directions,
|
|
1672
|
+
* and the direction follows where the answer is.
|
|
1673
|
+
*
|
|
1674
|
+
* This is the DECLARED REVERSAL of VD-5 for every tool but the shell
|
|
1675
|
+
* (0.22.0 reversed the shell alone): VD-5 collapsed a settled call to
|
|
1676
|
+
* its head row because ungrounded output rows owned the screen, and
|
|
1677
|
+
* the card is what changes that arithmetic — the rows are inside a
|
|
1678
|
+
* surface that says where the call begins and ends. */
|
|
1679
|
+
function previewHead(text, W, tone) {
|
|
1680
|
+
const rows = blockRows(text, W, tone);
|
|
1681
|
+
if (rows.length <= CAP_PREVIEW)
|
|
1682
|
+
return rows;
|
|
1683
|
+
return [...rows.slice(0, CAP_PREVIEW), ...cutNote(rows.length - CAP_PREVIEW, "more", W, tone)];
|
|
1684
|
+
}
|
|
1685
|
+
/** The preview's cut note, in one place so the head and the tail
|
|
1686
|
+
* directions cannot drift apart. The KEY is reserved (TUI2-R1.5 ⑤): a
|
|
1687
|
+
* row that says how much is hidden without saying how to see it is the
|
|
1688
|
+
* silence the affordance exists to remove. */
|
|
1689
|
+
function cutNote(cut, word, W, tone) {
|
|
1690
|
+
const n = `${cut} ${word} line${cut === 1 ? "" : "s"}`;
|
|
1691
|
+
return noteRow(pickTier([`… ${n} · ctrl+o expands`, `… ${n} · ctrl+o`, `… ${cut} · ctrl+o`, "· ctrl+o"], W - visibleWidth(noteIndent())), W, tone);
|
|
1840
1692
|
}
|
|
1841
1693
|
/** The error text head: the FIRST rows, capped at 3 — the answer is at
|
|
1842
1694
|
* the start (opencode's collapseToolOutput direction). The header row
|
|
@@ -1852,81 +1704,77 @@ function errorBody(c, W) {
|
|
|
1852
1704
|
// FULL content including the "[Permission denied] " prefix (never
|
|
1853
1705
|
// hide information — the folded body rides the pinned row).
|
|
1854
1706
|
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];
|
|
1707
|
+
// R13 — a failure is a CARD like any other call: the same cap of
|
|
1708
|
+
// five, the same note wording, the same direction (an error's answer
|
|
1709
|
+
// is at the top). It kept a cap of three and `+2 more · ctrl+o` from
|
|
1710
|
+
// before the card existed, which made the one shape a reader most
|
|
1711
|
+
// wants to read the one shape that showed least of itself.
|
|
1712
|
+
return previewHead(c.resultText.split("\n").slice(skipFirst).join("\n"), W, slabPaints() ? "body" : "dim");
|
|
1887
1713
|
}
|
|
1888
1714
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
1894
|
-
*
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1715
|
+
* DC-46 — THE RUNNING WINDOW GROWS, and nothing pads it.
|
|
1716
|
+
*
|
|
1717
|
+
* DECLARED REVERSAL of W8's fixed window and of E2 as first written
|
|
1718
|
+
* ("allocated at the settled card's height, and only ever shrinks at
|
|
1719
|
+
* settle"). Both fixed a height so it would not move while a command
|
|
1720
|
+
* ran; both fixed it at a height the SETTLE then changed, and the settle
|
|
1721
|
+
* is where the cost landed. A card allocated at twelve rows and settling
|
|
1722
|
+
* at three gives nine rows back, the window's top is clamped and cannot
|
|
1723
|
+
* follow, and the difference is a blank band above the composer.
|
|
1724
|
+
* Measured on the a7 replay: hole-frames 8.9 / 13.5 / 3.8 percent at
|
|
1725
|
+
* 0.23.0 against 16.9 / 24.6 / 7.9 with the shrink. The only source is
|
|
1726
|
+
* the shrink, so the cure is to stop shrinking.
|
|
1727
|
+
*
|
|
1728
|
+
* So the window IS its content: one row while nothing has arrived, one
|
|
1729
|
+
* more per line to the cap, then the cut note above a scrolling tail.
|
|
1730
|
+
* R7a's "blank, not a bar" retires with the padding it governed — it was
|
|
1731
|
+
* about what to draw on rows a FIXED height reserved, and no height is
|
|
1732
|
+
* reserved now.
|
|
1733
|
+
*
|
|
1734
|
+
* The direction is the SETTLED card's, so a settle swaps content and
|
|
1735
|
+
* moves nothing: a shell shows its tail with the note above, everything
|
|
1736
|
+
* else its head with the note below.
|
|
1904
1737
|
*/
|
|
1905
|
-
function
|
|
1906
|
-
if (text === "")
|
|
1907
|
-
return liveWindow("", W);
|
|
1738
|
+
function liveWindow(c, W, cap, tone) {
|
|
1908
1739
|
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
|
-
|
|
1740
|
+
// TUI2-R1.5 ④(b) (VD-4): leading empty lines in the sidecar (a
|
|
1741
|
+
// 4096-byte tail can begin on a line boundary) are skipped, so the
|
|
1742
|
+
// output starts under its own header.
|
|
1743
|
+
const all = blockRows(c.resultText, W, tone);
|
|
1744
|
+
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1745
|
+
// DC-46, derived — NOTHING YET IS NO WINDOW AT ALL, so a running call
|
|
1746
|
+
// with no output is the same THREE-ROW card as a settled one with
|
|
1747
|
+
// none. The ruling's skeleton put a `waiting for output` row here; a
|
|
1748
|
+
// command that returns nothing (`true`, a silent build) would then
|
|
1749
|
+
// settle from seven rows to three, which is the very shrink the ruling
|
|
1750
|
+
// exists to remove — its own rule cannot hold with that row in place.
|
|
1751
|
+
//
|
|
1752
|
+
// Nothing is lost: the breathing mark says the call is in flight and
|
|
1753
|
+
// the status row's elapsed says how long, so a row reading "waiting
|
|
1754
|
+
// for output" carries no fact they do not (§1.3). The card grows the
|
|
1755
|
+
// instant a line arrives, and growth is what this design permits.
|
|
1756
|
+
if (from < 0)
|
|
1757
|
+
return [];
|
|
1758
|
+
const rows = all.slice(from);
|
|
1759
|
+
if (rows.length <= cap)
|
|
1760
|
+
return rows;
|
|
1761
|
+
return c.name === "shell"
|
|
1762
|
+
? [...cutNote(rows.length - cap, "earlier", W, tone), ...rows.slice(rows.length - cap)]
|
|
1763
|
+
: [...rows.slice(0, cap), ...cutNote(rows.length - cap, "more", W, tone)];
|
|
1764
|
+
}
|
|
1765
|
+
/** DC-46 — the window's HIGH-WATER, per cell: the room a frame leaves
|
|
1766
|
+
* caps how far a window may GROW, and never shrinks one that already
|
|
1767
|
+
* grew. Without this a second call starting would pull the first's
|
|
1768
|
+
* window in, which is the same shrink by another route. Keyed on the
|
|
1769
|
+
* cell, like `blockMemo`, and it only ever matters while the cell is
|
|
1770
|
+
* live — a settled cell renders from its result alone. */
|
|
1771
|
+
const liveHighWater = new WeakMap();
|
|
1772
|
+
function liveCap(c, room) {
|
|
1773
|
+
const want = Math.min(CAP_PREVIEW, Math.max(1, room));
|
|
1774
|
+
const held = liveHighWater.get(c) ?? 0;
|
|
1775
|
+
const cap = Math.max(want, held);
|
|
1776
|
+
liveHighWater.set(c, cap);
|
|
1777
|
+
return cap;
|
|
1930
1778
|
}
|
|
1931
1779
|
/**
|
|
1932
1780
|
* R4 — the standing act slot.
|
|
@@ -1972,7 +1820,7 @@ export function slotTail(text, W, rows) {
|
|
|
1972
1820
|
return [];
|
|
1973
1821
|
const p = palette();
|
|
1974
1822
|
const all = blockRows(text, W);
|
|
1975
|
-
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(
|
|
1823
|
+
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1976
1824
|
const body = from < 0 ? [] : all.slice(from);
|
|
1977
1825
|
// R7a: no pad. The slot stopped padding (see slotPad) and this was
|
|
1978
1826
|
// the same pad by another route — three blank rows under a call with
|
|
@@ -2133,7 +1981,18 @@ class MarkdownBlock {
|
|
|
2133
1981
|
this.cell = cell;
|
|
2134
1982
|
}
|
|
2135
1983
|
render(W, _ctx) {
|
|
2136
|
-
|
|
1984
|
+
// R13 E3 — THE MODEL'S WORDS MOVE TO COLUMN 2. A card's rows sit
|
|
1985
|
+
// there (E4) and the chip's text does too (D4), so prose at column
|
|
1986
|
+
// 0 would leave the page with three left edges for three registers
|
|
1987
|
+
// — the opposite of one rhythm. The registers are told apart by
|
|
1988
|
+
// SURFACE, which is §1.6's argument; the column is not one of the
|
|
1989
|
+
// things doing that work.
|
|
1990
|
+
//
|
|
1991
|
+
// The block folds in the room the indent leaves, so invariant ①
|
|
1992
|
+
// holds by construction. A block's own leading blank (its `gap`)
|
|
1993
|
+
// stays EMPTY: an indented blank row is trailing whitespace, and
|
|
1994
|
+
// §1.3 forbids a mark on a row with nothing to mark.
|
|
1995
|
+
return renderBlock(this.cell.block, Math.max(1, W - PROSE_COL.length)).map((r) => (r === "" ? r : `${PROSE_COL}${r}`));
|
|
2137
1996
|
}
|
|
2138
1997
|
}
|
|
2139
1998
|
/** The notice lines — the error surface. */
|