@vincemakes/kiso-tui 0.16.7 → 0.17.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 +1 -1
- package/dist/components.js +1 -1
- package/dist/compositor.js +379 -85
- package/package.json +2 -2
package/dist/components.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Amendment 4) — this module is the re-export shim: the compositor's
|
|
3
3
|
* and index.ts's imports (./components.js) stay verbatim. */
|
|
4
4
|
export * from "@vincemakes/kiso-tui-cells/components";
|
|
5
|
-
export { foldTerms } from "@vincemakes/kiso-tui-cells/components";
|
|
5
|
+
export { foldCountsObjects, foldTerms } from "@vincemakes/kiso-tui-cells/components";
|
|
6
6
|
export { MOTION_FRAMES, TWINKLE, breathFrame, twinkleFrame } from "@vincemakes/kiso-tui-cells/render";
|
package/dist/components.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
export * from "@vincemakes/kiso-tui-cells/components";
|
|
5
5
|
// R3 (design §5.2): the two motion cycles reach the compositor through
|
|
6
6
|
// the same shim every other cell primitive does.
|
|
7
|
-
export { foldTerms } from "@vincemakes/kiso-tui-cells/components";
|
|
7
|
+
export { foldCountsObjects, foldTerms } from "@vincemakes/kiso-tui-cells/components";
|
|
8
8
|
export { MOTION_FRAMES, TWINKLE, breathFrame, twinkleFrame } from "@vincemakes/kiso-tui-cells/render";
|
package/dist/compositor.js
CHANGED
|
@@ -52,7 +52,7 @@ import { MOUSE_OFF } from "./editor.js";
|
|
|
52
52
|
import { atPanelRows, bandHeader } from "./at-picker.js";
|
|
53
53
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
54
54
|
import { sessionPickerRows } from "./session-picker.js";
|
|
55
|
-
import { Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
55
|
+
import { Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, stretchLine, turnFold, visibleWidth, twinkleFrame, } from "./components.js";
|
|
56
56
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
57
57
|
import { displayVerb, keysSheetRows } from "./strings.js";
|
|
58
58
|
/** The cursor marker — an APC private sequence the focus component
|
|
@@ -69,6 +69,11 @@ const NOT_PAINTED = "\u0000never";
|
|
|
69
69
|
* that a single resize still feels immediate. */
|
|
70
70
|
const RESIZE_SETTLE_MS = 80;
|
|
71
71
|
const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
|
|
72
|
+
/** R3i — how many calls in flight the act window shows at once. Beyond
|
|
73
|
+
* it the block would grow with the model's parallelism, which is the
|
|
74
|
+
* same unbounded height the projection exists to remove; the rest are
|
|
75
|
+
* COUNTED, never dropped silently. */
|
|
76
|
+
const LIVE_ACT_HEADS = 3;
|
|
72
77
|
/** W13 / TUI2-R1 (B) — a rolled run's TITLE: the exploration sentence on
|
|
73
78
|
* a mixed run, W13's verb+count on a single-name one. */
|
|
74
79
|
function rolledTitle(cell) {
|
|
@@ -174,7 +179,7 @@ function openSegment(turn, now) {
|
|
|
174
179
|
const last = turn.segments[turn.segments.length - 1];
|
|
175
180
|
if (last !== undefined && last.closedAt === null)
|
|
176
181
|
return last;
|
|
177
|
-
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), folded: false, spilled: false, headCell: null, cells: [] };
|
|
182
|
+
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), thinkingMs: 0, thinkingSince: null, folded: false, spilled: false, headCell: null, cells: [] };
|
|
178
183
|
turn.segments.push(fresh);
|
|
179
184
|
return fresh;
|
|
180
185
|
}
|
|
@@ -183,8 +188,21 @@ function openSegment(turn, now) {
|
|
|
183
188
|
* keeps a zero-cell segment from ever existing. */
|
|
184
189
|
function closeSegment(turn, now) {
|
|
185
190
|
const last = turn?.segments[turn.segments.length - 1];
|
|
186
|
-
if (last
|
|
187
|
-
|
|
191
|
+
if (last === undefined || last.closedAt !== null)
|
|
192
|
+
return;
|
|
193
|
+
stopThinking(last, now);
|
|
194
|
+
last.closedAt = now;
|
|
195
|
+
}
|
|
196
|
+
/** R3i — the segment's thinking clock stops. It runs from the first
|
|
197
|
+
* thinking delta of a stretch and stops at the first NON-thinking
|
|
198
|
+
* event, the same rule the CLI applies to the turn — so `thought Ns`
|
|
199
|
+
* is thinking time at every scale and never a wall clock wearing the
|
|
200
|
+
* word (the R3g defect, kept closed at the new scale). */
|
|
201
|
+
function stopThinking(seg, now) {
|
|
202
|
+
if (seg === undefined || seg.thinkingSince === null)
|
|
203
|
+
return;
|
|
204
|
+
seg.thinkingMs += Math.max(0, now - seg.thinkingSince);
|
|
205
|
+
seg.thinkingSince = null;
|
|
188
206
|
}
|
|
189
207
|
/** W20 — the whole-table-replace comparison: the live task block only
|
|
190
208
|
* redraws when the items actually changed (the task extension's
|
|
@@ -442,7 +460,7 @@ export class Body {
|
|
|
442
460
|
// W14: the turn boundary — the record the fold-hold's release
|
|
443
461
|
// state machine reads; the cell carries the record's index. A9:
|
|
444
462
|
// the user's own words ride the record — the fold's leading chip.
|
|
445
|
-
this.#turns.push({ ended: false, hasText: false, thoughtSeconds: 0, reads: 0, edits: 0, others: new Map(), words: text, folded: false, segments: [] });
|
|
463
|
+
this.#turns.push({ ended: false, hasText: false, thoughtSeconds: 0, reads: 0, edits: 0, others: new Map(), seen: new Map(), words: text, folded: false, segments: [] });
|
|
446
464
|
this.#cells.push({ kind: "user", text, done: true, turn: this.#turns.length - 1 });
|
|
447
465
|
this.#mark();
|
|
448
466
|
}
|
|
@@ -466,7 +484,20 @@ export class Body {
|
|
|
466
484
|
// the cell belongs to, and a stamp taken first records the
|
|
467
485
|
// PREVIOUS segment (or none at all) — which left the thinking
|
|
468
486
|
// row standing outside the fold it should have led.
|
|
469
|
-
openSegment(this.#turns[this.#turns.length - 1], Date.now());
|
|
487
|
+
const seg = openSegment(this.#turns[this.#turns.length - 1], Date.now());
|
|
488
|
+
// R3i: the stretch's thinking clock starts HERE — at the first
|
|
489
|
+
// delta of this stretch, the same moment the CLI starts the
|
|
490
|
+
// turn's — and stops at the next non-thinking event below.
|
|
491
|
+
if (seg !== null && seg.thinkingSince === null)
|
|
492
|
+
seg.thinkingSince = Date.now();
|
|
493
|
+
// R3i: and the beat starts HERE. Law 1.4 says "a running thought
|
|
494
|
+
// twinkles", and `#armSpinner`'s own predicate has always
|
|
495
|
+
// included an open thinking cell — but the only caller was
|
|
496
|
+
// `toolRunning`, so a stretch that thought and did nothing else
|
|
497
|
+
// never moved at all. The line's seconds are a frame-time
|
|
498
|
+
// derivation, so without the beat they also never ticked: the
|
|
499
|
+
// row read `thinking 0s` for as long as the model thought.
|
|
500
|
+
this.#armSpinner();
|
|
470
501
|
this.#stampSegment();
|
|
471
502
|
}
|
|
472
503
|
this.#mark();
|
|
@@ -474,6 +505,10 @@ export class Body {
|
|
|
474
505
|
thinkingEnd() {
|
|
475
506
|
const last = this.#cells[this.#cells.length - 1];
|
|
476
507
|
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
508
|
+
// R3i: every closer — text, a notice, a terminal label, the next
|
|
509
|
+
// turn — routes through here, so the clock cannot keep running
|
|
510
|
+
// past the thing that ended it.
|
|
511
|
+
stopThinking(this.#turns[this.#turns.length - 1]?.segments.at(-1), Date.now());
|
|
477
512
|
last.done = true;
|
|
478
513
|
this.#lastThinking = last.text;
|
|
479
514
|
if (!this.#isActive())
|
|
@@ -520,16 +555,56 @@ export class Body {
|
|
|
520
555
|
// order). The CLI's recap counts the same way (edit_file).
|
|
521
556
|
const turn = this.#turns[this.#turns.length - 1];
|
|
522
557
|
if (turn !== undefined) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
558
|
+
// R3h (fable, 2026-08-29): an OBJECT-counting tool counts the
|
|
559
|
+
// distinct thing, not the act. Reading one file twice used to
|
|
560
|
+
// fold as `read 2 files` — a sentence law 1.3 forbids, and one
|
|
561
|
+
// this product shipped. `bump` is false on the second sighting
|
|
562
|
+
// of a target the term has already counted; an ACT-counting
|
|
563
|
+
// tool (a search, a shell command) always bumps, because two
|
|
564
|
+
// searches for the same pattern really are two searches.
|
|
565
|
+
// R3i phase 5 — an ANSWER is words, and words do not fold (law
|
|
566
|
+
// 1.7). `ask_user` closes the open stretch exactly as prose
|
|
567
|
+
// does, and never joins one: absorbed into `1 × ask_user`,
|
|
568
|
+
// what the human said would be gone from the screen — and the
|
|
569
|
+
// one thing a summary must not do is speak for the human.
|
|
570
|
+
if (name === "ask_user") {
|
|
571
|
+
// no stamp: it belongs to NO stretch, so no fold can speak
|
|
572
|
+
// for it — the same standing a block of prose has.
|
|
573
|
+
closeSegment(turn, Date.now());
|
|
574
|
+
this.#mark();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
578
|
+
const bump = (rec) => {
|
|
579
|
+
if (target === null)
|
|
580
|
+
return true;
|
|
581
|
+
let set = rec.seen.get(name);
|
|
582
|
+
if (set === undefined) {
|
|
583
|
+
set = new Set();
|
|
584
|
+
rec.seen.set(name, set);
|
|
585
|
+
}
|
|
586
|
+
if (set.has(target))
|
|
587
|
+
return false;
|
|
588
|
+
set.add(target);
|
|
589
|
+
return true;
|
|
590
|
+
};
|
|
591
|
+
if (bump(turn)) {
|
|
592
|
+
if (name === "read_file")
|
|
593
|
+
turn.reads += 1;
|
|
594
|
+
else if (name === "edit_file")
|
|
595
|
+
turn.edits += 1;
|
|
596
|
+
else
|
|
597
|
+
turn.others.set(name, (turn.others.get(name) ?? 0) + 1);
|
|
598
|
+
}
|
|
529
599
|
// R3b: and into the SEGMENT, which opens here when this is the
|
|
530
|
-
// first work since the last text block.
|
|
600
|
+
// first work since the last text block. Its set is its OWN — a
|
|
601
|
+
// file read once per segment is one file in each segment's
|
|
602
|
+
// terms and one file in the turn's.
|
|
531
603
|
const seg = openSegment(turn, Date.now());
|
|
532
|
-
|
|
604
|
+
// R3i: a tool call is a NON-thinking event — the clock stops,
|
|
605
|
+
// exactly as the CLI's does at the same boundary.
|
|
606
|
+
stopThinking(seg ?? undefined, Date.now());
|
|
607
|
+
if (seg !== null && bump(seg)) {
|
|
533
608
|
if (name === "read_file")
|
|
534
609
|
seg.reads += 1;
|
|
535
610
|
else if (name === "edit_file")
|
|
@@ -1050,7 +1125,20 @@ export class Body {
|
|
|
1050
1125
|
// boundaries survive where they carry meaning (the write that
|
|
1051
1126
|
// splits two explore runs); they simply no longer bound what
|
|
1052
1127
|
// the key can reach.
|
|
1053
|
-
|
|
1128
|
+
// DECLARED SUPERSESSION (R3i phase 3) — the expansion covers THIS
|
|
1129
|
+
// STRETCH, and only this stretch.
|
|
1130
|
+
//
|
|
1131
|
+
// R3f widened it to the whole turn, and had to: R3d had made
|
|
1132
|
+
// the fold the TURN's while the expansion still walked one
|
|
1133
|
+
// segment, so a line claiming `read 3 files · edited 1 file`
|
|
1134
|
+
// opened only the reads — work named and then withheld, the one
|
|
1135
|
+
// thing this file's first gate forbids. R3i moves the fold back
|
|
1136
|
+
// to the stretch, so the pairing is exact again: every stretch
|
|
1137
|
+
// has its OWN line and its own key, and each key opens the work
|
|
1138
|
+
// its line named. Keeping the turn walk would break the same
|
|
1139
|
+
// rule from the other side — two lines, each opening
|
|
1140
|
+
// everything, each header describing rows the other also shows.
|
|
1141
|
+
for (const j of seg.cells) {
|
|
1054
1142
|
if (j < idx)
|
|
1055
1143
|
continue;
|
|
1056
1144
|
const c = this.#cells[j];
|
|
@@ -1073,8 +1161,26 @@ export class Body {
|
|
|
1073
1161
|
// the header names what the FOLD said — the turn's terms — so the
|
|
1074
1162
|
// line you pressed and the block it opens agree. It used to name
|
|
1075
1163
|
// segment 1's, which contradicted the fold above it.
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1164
|
+
const t = this.#stretchTerms(seg);
|
|
1165
|
+
const head = foldTerms(t.calls.find(([n]) => n === "read_file")?.[1] ?? 0, t.calls.find(([n]) => n === "edit_file")?.[1] ?? 0, t.calls.filter(([n]) => n !== "read_file" && n !== "edit_file"));
|
|
1166
|
+
// R3i phase 4 — THE FOOTER TELLS THE TRUTH ABOUT THIS PATH.
|
|
1167
|
+
//
|
|
1168
|
+
// The rows come from the rollup's own projection, whose last
|
|
1169
|
+
// row reads `└ ctrl+r collapses` — true where it was written
|
|
1170
|
+
// (the LIVE toggle, which really does close again) and false
|
|
1171
|
+
// here. A committed row is ink: ADR-0046 forbids rewriting
|
|
1172
|
+
// history, so nothing about this block can be taken back. The
|
|
1173
|
+
// next press opens the NEXT fold, and the row now says so.
|
|
1174
|
+
const closing = rows.length > 0 && /ctrl\+r collapses/.test(rows[rows.length - 1] ?? "");
|
|
1175
|
+
const body = closing ? rows.slice(0, -1) : rows;
|
|
1176
|
+
return {
|
|
1177
|
+
kind: "appended",
|
|
1178
|
+
lines: [
|
|
1179
|
+
`${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
|
|
1180
|
+
...body,
|
|
1181
|
+
` ${p.dim}└ end of expansion · ctrl+r opens the next fold${p.reset}`,
|
|
1182
|
+
],
|
|
1183
|
+
};
|
|
1078
1184
|
}
|
|
1079
1185
|
if (cell.kind !== "tool")
|
|
1080
1186
|
return { kind: "none" };
|
|
@@ -1549,6 +1655,118 @@ export class Body {
|
|
|
1549
1655
|
this.render();
|
|
1550
1656
|
}
|
|
1551
1657
|
// ---- the one writer ----
|
|
1658
|
+
/**
|
|
1659
|
+
* R3i phase 2 — THE LIVE PROJECTION.
|
|
1660
|
+
*
|
|
1661
|
+
* One definition, called from the natural path and from inside the
|
|
1662
|
+
* force-commit loop, because two copies of "what the live region
|
|
1663
|
+
* looks like" is two answers to one question.
|
|
1664
|
+
*
|
|
1665
|
+
* The change this phase makes, and the ONLY one: the cells of the
|
|
1666
|
+
* OPEN stretch no longer each hold a row. The stretch is one line —
|
|
1667
|
+
* the same line the settle will keep, in the present tense — plus
|
|
1668
|
+
* the calls actually in flight. A completed call renders nothing;
|
|
1669
|
+
* its count rides the line.
|
|
1670
|
+
*
|
|
1671
|
+
* What it fixes: a 28-call turn used to spend 28 rows of a 30-row
|
|
1672
|
+
* live region, so overflow was the NORM on real turns rather than
|
|
1673
|
+
* the edge — and a turn that overflows may not fold (R3f: a line
|
|
1674
|
+
* cannot claim rows already in the scrollback), which is why the
|
|
1675
|
+
* fold missed exactly the turns it exists for. The block's height
|
|
1676
|
+
* no longer depends on the call count at all.
|
|
1677
|
+
*
|
|
1678
|
+
* What it does NOT change: nothing about what commits or when. The
|
|
1679
|
+
* hold is untouched, the force-commit cap is untouched, and the
|
|
1680
|
+
* settle still produces the same fold it did before. That is the
|
|
1681
|
+
* charter's line between this phase and the next.
|
|
1682
|
+
*/
|
|
1683
|
+
#liveProjection(W, ctx) {
|
|
1684
|
+
const out = [];
|
|
1685
|
+
const focus = this.#focusIndex();
|
|
1686
|
+
const turn = this.#turns[this.#turns.length - 1];
|
|
1687
|
+
const open = turn !== undefined && !turn.ended ? (turn.segments[turn.segments.length - 1] ?? null) : null;
|
|
1688
|
+
const openSeg = open !== null && open.closedAt === null ? open : null;
|
|
1689
|
+
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1690
|
+
let stretchDrawn = false;
|
|
1691
|
+
let runningShown = 0;
|
|
1692
|
+
let runningHidden = 0;
|
|
1693
|
+
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1694
|
+
const cell = this.#cells[i];
|
|
1695
|
+
const inOpen = openSeg !== null && openSeg.cells.includes(i);
|
|
1696
|
+
if (inOpen) {
|
|
1697
|
+
// the stretch's ONE line, drawn once, at its first cell
|
|
1698
|
+
if (!stretchDrawn) {
|
|
1699
|
+
stretchDrawn = true;
|
|
1700
|
+
const rows = stretchLine({ ...this.#stretchTerms(openSeg), phase: this.#stretchPhase(openSeg), mark: twinkleFrame(this.#spinnerI) }, W);
|
|
1701
|
+
out.push(...this.#space(i, prev, rows));
|
|
1702
|
+
prev = rows;
|
|
1703
|
+
}
|
|
1704
|
+
// a DONE cell's row is gone; its count is on the line above.
|
|
1705
|
+
// A cell still in flight keeps its row and its output —
|
|
1706
|
+
// hiding the work in flight would be the opposite defect.
|
|
1707
|
+
const flight = cell.kind === "tool" && !cell.done;
|
|
1708
|
+
if (!flight)
|
|
1709
|
+
continue;
|
|
1710
|
+
if (runningShown >= LIVE_ACT_HEADS) {
|
|
1711
|
+
runningHidden += 1;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
runningShown += 1;
|
|
1715
|
+
}
|
|
1716
|
+
const rows = cellComponent(cell).render(W, ctx);
|
|
1717
|
+
// the head row carries the affordance; the tint lands on it and
|
|
1718
|
+
// nowhere else, which is what makes "exactly one" structural
|
|
1719
|
+
if (i === focus && rows.length > 0)
|
|
1720
|
+
rows[0] = focusToken(rows[0], W);
|
|
1721
|
+
out.push(...this.#space(i, prev, rows));
|
|
1722
|
+
prev = rows;
|
|
1723
|
+
}
|
|
1724
|
+
if (runningHidden > 0) {
|
|
1725
|
+
const p = palette();
|
|
1726
|
+
out.push(` ${p.dim}└ +${runningHidden} more running${p.reset}`);
|
|
1727
|
+
}
|
|
1728
|
+
return out;
|
|
1729
|
+
}
|
|
1730
|
+
/** R3i — the open stretch's phase. It is THINKING while a thinking
|
|
1731
|
+
* cell of it is still open and no call has started; otherwise it is
|
|
1732
|
+
* ACTING. The tense follows the phase, and the phase is what the
|
|
1733
|
+
* human is watching happen. */
|
|
1734
|
+
#stretchPhase(seg) {
|
|
1735
|
+
return seg.thinkingSince !== null && seg.reads === 0 && seg.edits === 0 && seg.others.size === 0 ? "thinking" : "acting";
|
|
1736
|
+
}
|
|
1737
|
+
/** R3i — the open stretch's terms, in the shape the line renders. */
|
|
1738
|
+
#stretchTerms(seg) {
|
|
1739
|
+
const ms = seg.thinkingMs + (seg.thinkingSince === null ? 0 : Math.max(0, Date.now() - seg.thinkingSince));
|
|
1740
|
+
// R3i: a call in TROUBLE does not contribute to the work terms.
|
|
1741
|
+
// The counts are taken at toolStart, before the outcome is known,
|
|
1742
|
+
// so a denied write would otherwise fold as `wrote 1 file` beside
|
|
1743
|
+
// the clause admitting it was refused — the line saying, in one
|
|
1744
|
+
// breath, that the file was written and that it was not. The
|
|
1745
|
+
// trouble clause is where those calls are counted.
|
|
1746
|
+
const bad = new Map();
|
|
1747
|
+
for (const j of seg.cells) {
|
|
1748
|
+
const c = this.#cells[j];
|
|
1749
|
+
if (c === undefined || c.kind !== "tool" || !this.#cellInTrouble(j))
|
|
1750
|
+
continue;
|
|
1751
|
+
bad.set(c.name, (bad.get(c.name) ?? 0) + 1);
|
|
1752
|
+
}
|
|
1753
|
+
const net = (name, n) => Math.max(0, n - (bad.get(name) ?? 0));
|
|
1754
|
+
const calls = [];
|
|
1755
|
+
if (net("read_file", seg.reads) > 0)
|
|
1756
|
+
calls.push(["read_file", net("read_file", seg.reads)]);
|
|
1757
|
+
if (net("edit_file", seg.edits) > 0)
|
|
1758
|
+
calls.push(["edit_file", net("edit_file", seg.edits)]);
|
|
1759
|
+
for (const [name, n] of seg.others)
|
|
1760
|
+
if (net(name, n) > 0)
|
|
1761
|
+
calls.push([name, net(name, n)]);
|
|
1762
|
+
const targets = [];
|
|
1763
|
+
for (const j of seg.cells) {
|
|
1764
|
+
const c = this.#cells[j];
|
|
1765
|
+
if (c !== undefined && c.kind === "tool")
|
|
1766
|
+
targets.push(toolTarget(c.name, JSON.parse(c.inputFull)));
|
|
1767
|
+
}
|
|
1768
|
+
return { thoughtSeconds: Math.round(ms / 1000), calls, targets, trouble: this.#segmentTroubleTerms(seg) };
|
|
1769
|
+
}
|
|
1552
1770
|
/** The live region's scalar — the unit tests assert the cap directly
|
|
1553
1771
|
* (the e2e gate pins the screen consequence). W11: the formula's
|
|
1554
1772
|
* blanks are join artifacts — the count includes them (they are real
|
|
@@ -1696,18 +1914,7 @@ export class Body {
|
|
|
1696
1914
|
// by construction), so the marker can never point at a cell the
|
|
1697
1915
|
// key would not take — which is the only way a focus marker is
|
|
1698
1916
|
// worth having.
|
|
1699
|
-
|
|
1700
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1701
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1702
|
-
const cell = this.#cells[i];
|
|
1703
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1704
|
-
// the head row carries the affordance; the tint lands on it and
|
|
1705
|
-
// nowhere else, which is what makes "exactly one" structural
|
|
1706
|
-
if (i === focus && rows.length > 0)
|
|
1707
|
-
rows[0] = focusToken(rows[0], W);
|
|
1708
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1709
|
-
prev = rows;
|
|
1710
|
-
}
|
|
1917
|
+
liveLines = this.#liveProjection(W, ctx);
|
|
1711
1918
|
}
|
|
1712
1919
|
// 3. the FORCE commits — the live region's hard cap H−1: overflow
|
|
1713
1920
|
// commits the oldest live cell UNCONDITIONALLY (the one sharp
|
|
@@ -1726,21 +1933,9 @@ export class Body {
|
|
|
1726
1933
|
// visible above it.
|
|
1727
1934
|
this.#markSpilled(this.#committed);
|
|
1728
1935
|
this.#commitCell(this.#committed, W, ctx);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
// pointed at may have just left the live region
|
|
1733
|
-
const focus = this.#focusIndex();
|
|
1734
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1735
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1736
|
-
const cell = this.#cells[i];
|
|
1737
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1738
|
-
if (i === focus && rows.length > 0)
|
|
1739
|
-
rows[0] = focusToken(rows[0], W);
|
|
1740
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1741
|
-
prev = rows;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1936
|
+
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
1937
|
+
// pointed at may have just left the live region.
|
|
1938
|
+
liveLines = this.#liveProjection(W, ctx);
|
|
1744
1939
|
}
|
|
1745
1940
|
// 4. the geometry — the live region's first row:
|
|
1746
1941
|
// liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
|
|
@@ -1880,7 +2075,29 @@ export class Body {
|
|
|
1880
2075
|
#space(i, prev, rows) {
|
|
1881
2076
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
1882
2077
|
return rows;
|
|
1883
|
-
return bodySpacing(prev, rows);
|
|
2078
|
+
return bodySpacing(this.#lastDrawn(i, prev), rows);
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* R3i — the previous DRAWN sibling, not the previous cell.
|
|
2082
|
+
*
|
|
2083
|
+
* The spacing formula reads what stood above; a cell that rendered
|
|
2084
|
+
* nothing did not stand above anything. Since R3d whole families of
|
|
2085
|
+
* cells render `[]` — the members a fold speaks for — and the
|
|
2086
|
+
* formula was reading that empty array as "a zero-row sibling", so a
|
|
2087
|
+
* multi-row block following a fold lost the blank that belongs above
|
|
2088
|
+
* it. The defect predates this round (any folded turn followed by a
|
|
2089
|
+
* raw block has it); R3i's projection is what finally put a test on
|
|
2090
|
+
* the path.
|
|
2091
|
+
*/
|
|
2092
|
+
#lastDrawn(i, prev) {
|
|
2093
|
+
if (prev !== null && prev.length > 0)
|
|
2094
|
+
return prev;
|
|
2095
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
2096
|
+
const cached = this.#lineCache[j];
|
|
2097
|
+
if (cached !== null && cached !== undefined && cached.length > 0)
|
|
2098
|
+
return cached;
|
|
2099
|
+
}
|
|
2100
|
+
return prev;
|
|
1884
2101
|
}
|
|
1885
2102
|
/** Commit the cell at index i: render + cache its lines (immutable —
|
|
1886
2103
|
* the force-committed form freezes at the current render), advance
|
|
@@ -1937,6 +2154,67 @@ export class Body {
|
|
|
1937
2154
|
* denial keeps all twenty rows. The alternative is a screen that says
|
|
1938
2155
|
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
1939
2156
|
*/
|
|
2157
|
+
/**
|
|
2158
|
+
* R3i — the trouble the stretch met, as the line's own terms.
|
|
2159
|
+
*
|
|
2160
|
+
* Law 1.3: an outcome is stated in WORDS, "the only form that
|
|
2161
|
+
* survives a pipe". So the kind is a different word, never a
|
|
2162
|
+
* different colour — `2 failed`, `1 denied`, `1 interrupted` — and
|
|
2163
|
+
* the failure's identity rides with it. In this phase the terms are
|
|
2164
|
+
* only DRAWN (the live line names trouble the moment it happens);
|
|
2165
|
+
* whether trouble still blocks the fold is the next phase's ruling.
|
|
2166
|
+
*/
|
|
2167
|
+
/** R3i — is this cell one the fold must not count as work done? */
|
|
2168
|
+
#cellInTrouble(i) {
|
|
2169
|
+
const c = this.#cells[i];
|
|
2170
|
+
if (c === undefined || c.kind !== "tool")
|
|
2171
|
+
return false;
|
|
2172
|
+
return c.isError || c.reason !== null || c.verdict?.decision === "denied";
|
|
2173
|
+
}
|
|
2174
|
+
#segmentTroubleTerms(seg) {
|
|
2175
|
+
let failed = 0;
|
|
2176
|
+
let denied = 0;
|
|
2177
|
+
let interrupted = 0;
|
|
2178
|
+
let what = "";
|
|
2179
|
+
for (const j of seg.cells) {
|
|
2180
|
+
const c = this.#cells[j];
|
|
2181
|
+
if (c === undefined || c.kind !== "tool")
|
|
2182
|
+
continue;
|
|
2183
|
+
// WHICH call, and WHY. The target alone answers the first and
|
|
2184
|
+
// not the second, and for a policy denial the second is the
|
|
2185
|
+
// whole point: `sub/out.txt` does not tell a human that plan
|
|
2186
|
+
// mode is read-only, and that sentence is the one they act
|
|
2187
|
+
// on. Law 1.3's own words — an outcome is stated in words —
|
|
2188
|
+
// and the ladder cuts this clause last, so it degrades to the
|
|
2189
|
+
// target before it disappears.
|
|
2190
|
+
const named = () => {
|
|
2191
|
+
const t = toolTarget(c.name, JSON.parse(c.inputFull));
|
|
2192
|
+
const why = c.verdict?.reason ?? c.reason;
|
|
2193
|
+
return why === null || why === undefined || why === "" || why === "interrupted" ? t : `${t} (${why})`;
|
|
2194
|
+
};
|
|
2195
|
+
if (c.verdict?.decision === "denied" || (c.reason !== null && c.reason !== "interrupted" && /denied/i.test(c.reason))) {
|
|
2196
|
+
denied += 1;
|
|
2197
|
+
if (what === "")
|
|
2198
|
+
what = named();
|
|
2199
|
+
}
|
|
2200
|
+
else if (c.reason === "interrupted") {
|
|
2201
|
+
interrupted += 1;
|
|
2202
|
+
}
|
|
2203
|
+
else if (c.isError || c.reason !== null) {
|
|
2204
|
+
failed += 1;
|
|
2205
|
+
if (what === "")
|
|
2206
|
+
what = named();
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
const out = [];
|
|
2210
|
+
if (failed > 0)
|
|
2211
|
+
out.push(["failed", failed, what]);
|
|
2212
|
+
if (denied > 0)
|
|
2213
|
+
out.push(["denied", denied, what]);
|
|
2214
|
+
if (interrupted > 0)
|
|
2215
|
+
out.push(["interrupted", interrupted, ""]);
|
|
2216
|
+
return out;
|
|
2217
|
+
}
|
|
1940
2218
|
#segmentHasTrouble(seg) {
|
|
1941
2219
|
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
1942
2220
|
// exists for, and it was the one case the predicate could not
|
|
@@ -2005,6 +2283,11 @@ export class Body {
|
|
|
2005
2283
|
const cell = this.#cells[i];
|
|
2006
2284
|
if (cell.kind !== "thinking" && cell.kind !== "tool")
|
|
2007
2285
|
return false;
|
|
2286
|
+
// R3i phase 5: an answered question is WORDS (law 1.7). It commits
|
|
2287
|
+
// when it is done, like prose, and is never held for a fold that
|
|
2288
|
+
// is not going to speak for it.
|
|
2289
|
+
if (cell.kind === "tool" && cell.name === "ask_user")
|
|
2290
|
+
return false;
|
|
2008
2291
|
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2009
2292
|
if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
|
|
2010
2293
|
return false;
|
|
@@ -2027,15 +2310,27 @@ export class Body {
|
|
|
2027
2310
|
// The quiet turn is the same rule seen from one side: its single
|
|
2028
2311
|
// segment never closes until the settle, so it holds exactly as
|
|
2029
2312
|
// it always did.
|
|
2030
|
-
//
|
|
2031
|
-
//
|
|
2032
|
-
//
|
|
2033
|
-
//
|
|
2313
|
+
// DECLARED SUPERSESSION (R3i phase 3, owner-ruled) — THE HOLD IS
|
|
2314
|
+
// THE SEGMENT'S AGAIN.
|
|
2315
|
+
//
|
|
2316
|
+
// R3d had made it the TURN's, so a turn's whole work waited on the
|
|
2317
|
+
// settle and every one of its counts then landed on ONE line above
|
|
2318
|
+
// all of its prose. The shape the owner asked for is one summary
|
|
2319
|
+
// per stretch, standing with the prose that stretch led to — which
|
|
2320
|
+
// requires a stretch to commit when its own text arrives.
|
|
2321
|
+
//
|
|
2322
|
+
// R3d's stated reason for leaving the segment was R3b's disease (a
|
|
2323
|
+
// chatty model turning every call into its own `✦ thought 2s ·
|
|
2324
|
+
// 1 read` row); the cures are the two rules R3b never had — a fold
|
|
2325
|
+
// must absorb at least two rows, and a stretch of exactly one call
|
|
2326
|
+
// names its TARGET rather than its count.
|
|
2327
|
+
//
|
|
2328
|
+
// The force-commit path still overrides this (a stretch too big for
|
|
2034
2329
|
// the screen spills and renders normally); that is the honest
|
|
2035
2330
|
// degradation, marked `spilled`.
|
|
2036
2331
|
const seg = this.#segmentOf(i);
|
|
2037
2332
|
if (seg !== null)
|
|
2038
|
-
return
|
|
2333
|
+
return seg.closedAt === null;
|
|
2039
2334
|
// no segment (the pipe path's shape) — W14's original test, kept
|
|
2040
2335
|
// so a cell that never got a segment behaves as it used to.
|
|
2041
2336
|
if (!turn.ended && !turn.hasText)
|
|
@@ -2129,45 +2424,44 @@ export class Body {
|
|
|
2129
2424
|
// The quiet turn keeps its fold because there IS no recap line
|
|
2130
2425
|
// to carry it: a turn with no text is the fold, and W14's gates
|
|
2131
2426
|
// pin that shape.
|
|
2132
|
-
|
|
2133
|
-
|
|
2427
|
+
// DECLARED SUPERSESSION (R3i phase 3, owner-ruled) — THE FOLD IS
|
|
2428
|
+
// THE SEGMENT'S, AND TROUBLE DOES NOT STOP IT.
|
|
2429
|
+
//
|
|
2430
|
+
// ① the SEGMENT. R3d folded the turn; the owner's shape is one
|
|
2431
|
+
// summary per stretch of work, standing with the prose that
|
|
2432
|
+
// stretch led to. R3b's disease is answered by the two rules
|
|
2433
|
+
// below rather than by leaving the segment: a fold must
|
|
2434
|
+
// absorb at least TWO rows, and a stretch of exactly one
|
|
2435
|
+
// call names its target (see stretchTerms).
|
|
2436
|
+
//
|
|
2437
|
+
// ② TROUBLE. R3b refused to fold any run holding a failure,
|
|
2438
|
+
// and R3g extended that to interrupts. Law 1.3 governs
|
|
2439
|
+
// marks versus WORDS and never granted a failure a
|
|
2440
|
+
// permanent row; law 1.7 says "Work folds, words do not".
|
|
2441
|
+
// So the work folds and the outcome words ride the line —
|
|
2442
|
+
// `1 denied: .env` — and the human sees, without pressing
|
|
2443
|
+
// anything, that trouble happened, on which call, and what
|
|
2444
|
+
// happened. The stderr is behind the key, because it is
|
|
2445
|
+
// detail, not outcome. The cost R3b priced as rare measured
|
|
2446
|
+
// at 2 failures in 28 calls in the 0.16.7 dogfood, with
|
|
2447
|
+
// zero folds as the result.
|
|
2448
|
+
if (turn !== undefined && seg !== null && seg.closedAt !== null && !seg.spilled && seg.cells.length >= 2) {
|
|
2449
|
+
if (!seg.folded) {
|
|
2134
2450
|
seg.folded = true;
|
|
2135
2451
|
seg.headCell = i;
|
|
2136
2452
|
turn.folded = true;
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
// whole turn and the chip has nowhere else to be. In a
|
|
2140
|
-
// turn WITH text the chip cell commits on its own, so a
|
|
2141
|
-
// fold that repeated the words would put the user's line
|
|
2142
|
-
// on screen twice. The words take the fold's width budget
|
|
2143
|
-
// (turnFold is W-aware — the ONE row never trips
|
|
2144
|
-
// invariant ①).
|
|
2145
|
-
const quiet = turn.ended && !turn.hasText;
|
|
2146
|
-
// R3g (fable, 2026-08-28) — DECLARED SUPERSESSION: both
|
|
2147
|
-
// branches read the SAME number now, `thoughtSeconds`,
|
|
2148
|
-
// the measure the kernel took and handed to endTurn.
|
|
2149
|
-
// The non-quiet branch used to re-derive a wall clock
|
|
2150
|
-
// from the segment's opening and print it under the word
|
|
2151
|
-
// "thought" — a different quantity wearing the same
|
|
2152
|
-
// label: a turn that thought 1s and then ran a 40s shell
|
|
2153
|
-
// said "thought 41s". The fold only ever renders after
|
|
2154
|
-
// endTurn (the gate below requires `turn.ended`), so the
|
|
2155
|
-
// honest number is always available by the time it runs.
|
|
2453
|
+
// DECLARED SUPERSESSION (R3i phase 3) — A9 NARROWS: the
|
|
2454
|
+
// fold carries WORK, never the human's words.
|
|
2156
2455
|
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
//
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
thoughtSeconds: seconds,
|
|
2167
|
-
reads: turn.reads,
|
|
2168
|
-
edits: turn.edits,
|
|
2169
|
-
others: [...turn.others],
|
|
2170
|
-
}, W);
|
|
2456
|
+
// A9 put the user's words on the fold as the SGR-7 chip.
|
|
2457
|
+
// Measured under R3i, that prints them twice: the chip
|
|
2458
|
+
// BAND commits on the frame it is pushed (it always has —
|
|
2459
|
+
// `#held` exempts non-thinking/tool cells), so a quiet
|
|
2460
|
+
// turn showed ` x ` on its own row and ` x ` again inside
|
|
2461
|
+
// the fold directly beneath it. The band is the record of
|
|
2462
|
+
// what was asked; this line is the record of what was
|
|
2463
|
+
// done. One fact, one row, each.
|
|
2464
|
+
return stretchLine({ ...this.#stretchTerms(seg), phase: "settled" }, W);
|
|
2171
2465
|
}
|
|
2172
2466
|
return [];
|
|
2173
2467
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.17.0"
|
|
39
39
|
}
|
|
40
40
|
}
|