@vincemakes/kiso-tui 0.16.8 → 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/compositor.js +345 -76
- package/package.json +2 -2
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, foldCountsObjects, 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(), seen: 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
|
|
@@ -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())
|
|
@@ -527,6 +562,18 @@ export class Body {
|
|
|
527
562
|
// of a target the term has already counted; an ACT-counting
|
|
528
563
|
// tool (a search, a shell command) always bumps, because two
|
|
529
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
|
+
}
|
|
530
577
|
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
531
578
|
const bump = (rec) => {
|
|
532
579
|
if (target === null)
|
|
@@ -554,6 +601,9 @@ export class Body {
|
|
|
554
601
|
// file read once per segment is one file in each segment's
|
|
555
602
|
// terms and one file in the turn's.
|
|
556
603
|
const seg = openSegment(turn, Date.now());
|
|
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());
|
|
557
607
|
if (seg !== null && bump(seg)) {
|
|
558
608
|
if (name === "read_file")
|
|
559
609
|
seg.reads += 1;
|
|
@@ -1075,7 +1125,20 @@ export class Body {
|
|
|
1075
1125
|
// boundaries survive where they carry meaning (the write that
|
|
1076
1126
|
// splits two explore runs); they simply no longer bound what
|
|
1077
1127
|
// the key can reach.
|
|
1078
|
-
|
|
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) {
|
|
1079
1142
|
if (j < idx)
|
|
1080
1143
|
continue;
|
|
1081
1144
|
const c = this.#cells[j];
|
|
@@ -1098,8 +1161,26 @@ export class Body {
|
|
|
1098
1161
|
// the header names what the FOLD said — the turn's terms — so the
|
|
1099
1162
|
// line you pressed and the block it opens agree. It used to name
|
|
1100
1163
|
// segment 1's, which contradicted the fold above it.
|
|
1101
|
-
const
|
|
1102
|
-
|
|
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
|
+
};
|
|
1103
1184
|
}
|
|
1104
1185
|
if (cell.kind !== "tool")
|
|
1105
1186
|
return { kind: "none" };
|
|
@@ -1574,6 +1655,118 @@ export class Body {
|
|
|
1574
1655
|
this.render();
|
|
1575
1656
|
}
|
|
1576
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
|
+
}
|
|
1577
1770
|
/** The live region's scalar — the unit tests assert the cap directly
|
|
1578
1771
|
* (the e2e gate pins the screen consequence). W11: the formula's
|
|
1579
1772
|
* blanks are join artifacts — the count includes them (they are real
|
|
@@ -1721,18 +1914,7 @@ export class Body {
|
|
|
1721
1914
|
// by construction), so the marker can never point at a cell the
|
|
1722
1915
|
// key would not take — which is the only way a focus marker is
|
|
1723
1916
|
// worth having.
|
|
1724
|
-
|
|
1725
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1726
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1727
|
-
const cell = this.#cells[i];
|
|
1728
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1729
|
-
// the head row carries the affordance; the tint lands on it and
|
|
1730
|
-
// nowhere else, which is what makes "exactly one" structural
|
|
1731
|
-
if (i === focus && rows.length > 0)
|
|
1732
|
-
rows[0] = focusToken(rows[0], W);
|
|
1733
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1734
|
-
prev = rows;
|
|
1735
|
-
}
|
|
1917
|
+
liveLines = this.#liveProjection(W, ctx);
|
|
1736
1918
|
}
|
|
1737
1919
|
// 3. the FORCE commits — the live region's hard cap H−1: overflow
|
|
1738
1920
|
// commits the oldest live cell UNCONDITIONALLY (the one sharp
|
|
@@ -1751,21 +1933,9 @@ export class Body {
|
|
|
1751
1933
|
// visible above it.
|
|
1752
1934
|
this.#markSpilled(this.#committed);
|
|
1753
1935
|
this.#commitCell(this.#committed, W, ctx);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// pointed at may have just left the live region
|
|
1758
|
-
const focus = this.#focusIndex();
|
|
1759
|
-
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1760
|
-
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1761
|
-
const cell = this.#cells[i];
|
|
1762
|
-
const rows = cellComponent(cell).render(W, ctx);
|
|
1763
|
-
if (i === focus && rows.length > 0)
|
|
1764
|
-
rows[0] = focusToken(rows[0], W);
|
|
1765
|
-
liveLines.push(...this.#space(i, prev, rows));
|
|
1766
|
-
prev = rows;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
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);
|
|
1769
1939
|
}
|
|
1770
1940
|
// 4. the geometry — the live region's first row:
|
|
1771
1941
|
// liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
|
|
@@ -1905,7 +2075,29 @@ export class Body {
|
|
|
1905
2075
|
#space(i, prev, rows) {
|
|
1906
2076
|
if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
|
|
1907
2077
|
return rows;
|
|
1908
|
-
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;
|
|
1909
2101
|
}
|
|
1910
2102
|
/** Commit the cell at index i: render + cache its lines (immutable —
|
|
1911
2103
|
* the force-committed form freezes at the current render), advance
|
|
@@ -1962,6 +2154,67 @@ export class Body {
|
|
|
1962
2154
|
* denial keeps all twenty rows. The alternative is a screen that says
|
|
1963
2155
|
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
1964
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
|
+
}
|
|
1965
2218
|
#segmentHasTrouble(seg) {
|
|
1966
2219
|
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
1967
2220
|
// exists for, and it was the one case the predicate could not
|
|
@@ -2030,6 +2283,11 @@ export class Body {
|
|
|
2030
2283
|
const cell = this.#cells[i];
|
|
2031
2284
|
if (cell.kind !== "thinking" && cell.kind !== "tool")
|
|
2032
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;
|
|
2033
2291
|
const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
2034
2292
|
if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
|
|
2035
2293
|
return false;
|
|
@@ -2052,15 +2310,27 @@ export class Body {
|
|
|
2052
2310
|
// The quiet turn is the same rule seen from one side: its single
|
|
2053
2311
|
// segment never closes until the settle, so it holds exactly as
|
|
2054
2312
|
// it always did.
|
|
2055
|
-
//
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
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
|
|
2059
2329
|
// the screen spills and renders normally); that is the honest
|
|
2060
2330
|
// degradation, marked `spilled`.
|
|
2061
2331
|
const seg = this.#segmentOf(i);
|
|
2062
2332
|
if (seg !== null)
|
|
2063
|
-
return
|
|
2333
|
+
return seg.closedAt === null;
|
|
2064
2334
|
// no segment (the pipe path's shape) — W14's original test, kept
|
|
2065
2335
|
// so a cell that never got a segment behaves as it used to.
|
|
2066
2336
|
if (!turn.ended && !turn.hasText)
|
|
@@ -2154,45 +2424,44 @@ export class Body {
|
|
|
2154
2424
|
// The quiet turn keeps its fold because there IS no recap line
|
|
2155
2425
|
// to carry it: a turn with no text is the fold, and W14's gates
|
|
2156
2426
|
// pin that shape.
|
|
2157
|
-
|
|
2158
|
-
|
|
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) {
|
|
2159
2450
|
seg.folded = true;
|
|
2160
2451
|
seg.headCell = i;
|
|
2161
2452
|
turn.folded = true;
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
// whole turn and the chip has nowhere else to be. In a
|
|
2165
|
-
// turn WITH text the chip cell commits on its own, so a
|
|
2166
|
-
// fold that repeated the words would put the user's line
|
|
2167
|
-
// on screen twice. The words take the fold's width budget
|
|
2168
|
-
// (turnFold is W-aware — the ONE row never trips
|
|
2169
|
-
// invariant ①).
|
|
2170
|
-
const quiet = turn.ended && !turn.hasText;
|
|
2171
|
-
// R3g (fable, 2026-08-28) — DECLARED SUPERSESSION: both
|
|
2172
|
-
// branches read the SAME number now, `thoughtSeconds`,
|
|
2173
|
-
// the measure the kernel took and handed to endTurn.
|
|
2174
|
-
// The non-quiet branch used to re-derive a wall clock
|
|
2175
|
-
// from the segment's opening and print it under the word
|
|
2176
|
-
// "thought" — a different quantity wearing the same
|
|
2177
|
-
// label: a turn that thought 1s and then ran a 40s shell
|
|
2178
|
-
// said "thought 41s". The fold only ever renders after
|
|
2179
|
-
// endTurn (the gate below requires `turn.ended`), so the
|
|
2180
|
-
// 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.
|
|
2181
2455
|
//
|
|
2182
|
-
//
|
|
2183
|
-
//
|
|
2184
|
-
//
|
|
2185
|
-
//
|
|
2186
|
-
//
|
|
2187
|
-
//
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
thoughtSeconds: seconds,
|
|
2192
|
-
reads: turn.reads,
|
|
2193
|
-
edits: turn.edits,
|
|
2194
|
-
others: [...turn.others],
|
|
2195
|
-
}, 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);
|
|
2196
2465
|
}
|
|
2197
2466
|
return [];
|
|
2198
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
|
}
|