@vincemakes/kiso-tui 0.16.6 → 0.16.8
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 +200 -29
- package/dist/render.d.ts +32 -2
- package/dist/render.js +45 -2
- 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, turnFold, visibleWidth, } 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
|
|
@@ -174,7 +174,7 @@ function openSegment(turn, now) {
|
|
|
174
174
|
const last = turn.segments[turn.segments.length - 1];
|
|
175
175
|
if (last !== undefined && last.closedAt === null)
|
|
176
176
|
return last;
|
|
177
|
-
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), folded: false, spilled: false, headCell: null, cells: [] };
|
|
177
|
+
const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), folded: false, spilled: false, headCell: null, cells: [] };
|
|
178
178
|
turn.segments.push(fresh);
|
|
179
179
|
return fresh;
|
|
180
180
|
}
|
|
@@ -442,7 +442,7 @@ export class Body {
|
|
|
442
442
|
// W14: the turn boundary — the record the fold-hold's release
|
|
443
443
|
// state machine reads; the cell carries the record's index. A9:
|
|
444
444
|
// 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: [] });
|
|
445
|
+
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
446
|
this.#cells.push({ kind: "user", text, done: true, turn: this.#turns.length - 1 });
|
|
447
447
|
this.#mark();
|
|
448
448
|
}
|
|
@@ -520,16 +520,41 @@ export class Body {
|
|
|
520
520
|
// order). The CLI's recap counts the same way (edit_file).
|
|
521
521
|
const turn = this.#turns[this.#turns.length - 1];
|
|
522
522
|
if (turn !== undefined) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
523
|
+
// R3h (fable, 2026-08-29): an OBJECT-counting tool counts the
|
|
524
|
+
// distinct thing, not the act. Reading one file twice used to
|
|
525
|
+
// fold as `read 2 files` — a sentence law 1.3 forbids, and one
|
|
526
|
+
// this product shipped. `bump` is false on the second sighting
|
|
527
|
+
// of a target the term has already counted; an ACT-counting
|
|
528
|
+
// tool (a search, a shell command) always bumps, because two
|
|
529
|
+
// searches for the same pattern really are two searches.
|
|
530
|
+
const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
|
|
531
|
+
const bump = (rec) => {
|
|
532
|
+
if (target === null)
|
|
533
|
+
return true;
|
|
534
|
+
let set = rec.seen.get(name);
|
|
535
|
+
if (set === undefined) {
|
|
536
|
+
set = new Set();
|
|
537
|
+
rec.seen.set(name, set);
|
|
538
|
+
}
|
|
539
|
+
if (set.has(target))
|
|
540
|
+
return false;
|
|
541
|
+
set.add(target);
|
|
542
|
+
return true;
|
|
543
|
+
};
|
|
544
|
+
if (bump(turn)) {
|
|
545
|
+
if (name === "read_file")
|
|
546
|
+
turn.reads += 1;
|
|
547
|
+
else if (name === "edit_file")
|
|
548
|
+
turn.edits += 1;
|
|
549
|
+
else
|
|
550
|
+
turn.others.set(name, (turn.others.get(name) ?? 0) + 1);
|
|
551
|
+
}
|
|
529
552
|
// R3b: and into the SEGMENT, which opens here when this is the
|
|
530
|
-
// first work since the last text block.
|
|
553
|
+
// first work since the last text block. Its set is its OWN — a
|
|
554
|
+
// file read once per segment is one file in each segment's
|
|
555
|
+
// terms and one file in the turn's.
|
|
531
556
|
const seg = openSegment(turn, Date.now());
|
|
532
|
-
if (seg !== null) {
|
|
557
|
+
if (seg !== null && bump(seg)) {
|
|
533
558
|
if (name === "read_file")
|
|
534
559
|
seg.reads += 1;
|
|
535
560
|
else if (name === "edit_file")
|
|
@@ -765,6 +790,25 @@ export class Body {
|
|
|
765
790
|
break;
|
|
766
791
|
}
|
|
767
792
|
}
|
|
793
|
+
// R3g (fable D3, 2026-08-28): an INTERRUPTED tool never receives a
|
|
794
|
+
// result, so its cell stays `done: false` — and the commit loop
|
|
795
|
+
// stops at the first cell that is not done. One esc mid-tool
|
|
796
|
+
// therefore parked the commit pointer for the REST of the
|
|
797
|
+
// session: every later turn's rows piled up in the live region
|
|
798
|
+
// and only ever left it through the force-commit cap. The turn's
|
|
799
|
+
// end is the boundary that closes them, exactly as it closes an
|
|
800
|
+
// open thinking cell. `reason` is set so the row keeps its words
|
|
801
|
+
// AND so #segmentHasTrouble holds the turn unfolded — an
|
|
802
|
+
// interruption is trouble, and law 1.3 says trouble is never
|
|
803
|
+
// summarised away.
|
|
804
|
+
for (const c of this.#cells) {
|
|
805
|
+
if (c.kind === "tool" && !c.done) {
|
|
806
|
+
c.state = "done";
|
|
807
|
+
c.reason = "interrupted";
|
|
808
|
+
c.doneAt = Date.now();
|
|
809
|
+
c.done = true;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
768
812
|
// the QUIET turn: an open thinking cell closes at the boundary —
|
|
769
813
|
// its natural closer is the text's arrival (never comes here — the
|
|
770
814
|
// text-less turn), so without this the fold could never commit AT
|
|
@@ -963,7 +1007,8 @@ export class Body {
|
|
|
963
1007
|
// expand in this method does, and they are the cells' OWN renders,
|
|
964
1008
|
// so the expansion cannot drift from what was folded.
|
|
965
1009
|
const seg = this.#segmentOf(idx);
|
|
966
|
-
|
|
1010
|
+
const foldTurn = (cell.kind === "thinking" || cell.kind === "tool") && cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
1011
|
+
if (seg !== null && foldTurn !== undefined && seg.headCell === idx) {
|
|
967
1012
|
const p = palette();
|
|
968
1013
|
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
969
1014
|
const back = `${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
|
|
@@ -1015,7 +1060,22 @@ export class Body {
|
|
|
1015
1060
|
}
|
|
1016
1061
|
run = [];
|
|
1017
1062
|
};
|
|
1018
|
-
|
|
1063
|
+
// R3f — the expansion covers the WHOLE TURN, every segment.
|
|
1064
|
+
//
|
|
1065
|
+
// R3d moved the fold to the turn while the expansion kept
|
|
1066
|
+
// walking one segment, so a turn that spoke between calls
|
|
1067
|
+
// folded to a line claiming `3 reads · 1 edit · 1 shell` whose
|
|
1068
|
+
// key opened only the reads: the edit and the shell were on no
|
|
1069
|
+
// surface and reachable by no key. That is the one thing this
|
|
1070
|
+
// round's own first gate forbids — the work is never
|
|
1071
|
+
// unreachable — and it is worse than never folding, because the
|
|
1072
|
+
// line names work it then withholds.
|
|
1073
|
+
//
|
|
1074
|
+
// A run still BREAKS at a non-explore cell, so the segment
|
|
1075
|
+
// boundaries survive where they carry meaning (the write that
|
|
1076
|
+
// splits two explore runs); they simply no longer bound what
|
|
1077
|
+
// the key can reach.
|
|
1078
|
+
for (const j of foldTurn.segments.flatMap((sg) => sg.cells).sort((a, b) => a - b)) {
|
|
1019
1079
|
if (j < idx)
|
|
1020
1080
|
continue;
|
|
1021
1081
|
const c = this.#cells[j];
|
|
@@ -1035,7 +1095,11 @@ export class Body {
|
|
|
1035
1095
|
// terms; each run below states what IT did, in the rollup's. Two
|
|
1036
1096
|
// scales, one wording each — the header used to borrow the run's
|
|
1037
1097
|
// sentence, which read as the same run twice.
|
|
1038
|
-
|
|
1098
|
+
// the header names what the FOLD said — the turn's terms — so the
|
|
1099
|
+
// line you pressed and the block it opens agree. It used to name
|
|
1100
|
+
// segment 1's, which contradicted the fold above it.
|
|
1101
|
+
const head = foldTerms(foldTurn.reads, foldTurn.edits, [...foldTurn.others]);
|
|
1102
|
+
return { kind: "appended", lines: [`${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`, ...rows] };
|
|
1039
1103
|
}
|
|
1040
1104
|
if (cell.kind !== "tool")
|
|
1041
1105
|
return { kind: "none" };
|
|
@@ -1675,6 +1739,17 @@ export class Body {
|
|
|
1675
1739
|
// edge — the cap scalar is asserted by the gates). W22: the
|
|
1676
1740
|
// queue band shrinks the cap by its rows (empty queue → H−4).
|
|
1677
1741
|
while (liveLines.length > H - 4 - inputExtra - queueRows.length && this.#committed < this.#cells.length) { // V6-3: the content cap H−4 (KC1: −N's extra rows)
|
|
1742
|
+
// R3f: the cell about to be force-committed marks its segment
|
|
1743
|
+
// SPILLED. The rule was written at R3b — "a segment too big for
|
|
1744
|
+
// the screen already has rows in the scrollback that cannot be
|
|
1745
|
+
// taken back, so it renders normally and does not collapse" —
|
|
1746
|
+
// and then never wired: `spilled` had a declaration, an
|
|
1747
|
+
// initializer and a read, and nothing ever set it. The read was
|
|
1748
|
+
// therefore vacuously true, so a 43-call turn force-committed
|
|
1749
|
+
// thirty expanded rows and STILL printed `✦ thought 103s · 43
|
|
1750
|
+
// reads` underneath them, claiming as folded the work standing
|
|
1751
|
+
// visible above it.
|
|
1752
|
+
this.#markSpilled(this.#committed);
|
|
1678
1753
|
this.#commitCell(this.#committed, W, ctx);
|
|
1679
1754
|
liveLines = [];
|
|
1680
1755
|
{
|
|
@@ -1888,7 +1963,12 @@ export class Body {
|
|
|
1888
1963
|
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
1889
1964
|
*/
|
|
1890
1965
|
#segmentHasTrouble(seg) {
|
|
1891
|
-
|
|
1966
|
+
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
1967
|
+
// exists for, and it was the one case the predicate could not
|
|
1968
|
+
// see — a denial carrying no `reason` string leaves isError
|
|
1969
|
+
// false and reason null, so `✦ thought 3s · 20 reads` could
|
|
1970
|
+
// stand over a refused write. The verdict is the record of it.
|
|
1971
|
+
return this.#segmentTools(seg).some((c) => c.isError || c.reason !== null || c.verdict?.decision === "denied");
|
|
1892
1972
|
}
|
|
1893
1973
|
/** R3b — the segment's TOOL cells, in order. */
|
|
1894
1974
|
#segmentTools(seg) {
|
|
@@ -1900,6 +1980,30 @@ export class Body {
|
|
|
1900
1980
|
}
|
|
1901
1981
|
return out;
|
|
1902
1982
|
}
|
|
1983
|
+
/** R3f — the cell is leaving the live region under the screen's hard
|
|
1984
|
+
* cap, so its segment can no longer be represented by a fold. */
|
|
1985
|
+
#markSpilled(i) {
|
|
1986
|
+
const seg = this.#segmentOf(i);
|
|
1987
|
+
if (seg !== null)
|
|
1988
|
+
seg.spilled = true;
|
|
1989
|
+
}
|
|
1990
|
+
/** R3f — did ANY of the turn's segments spill? The fold is the
|
|
1991
|
+
* TURN's, so one spilled segment makes the whole turn unfoldable:
|
|
1992
|
+
* a line claiming the turn's counts cannot stand under rows that
|
|
1993
|
+
* already show part of that same work. */
|
|
1994
|
+
#turnSpilled(turn) {
|
|
1995
|
+
return turn.segments.some((seg) => seg.spilled);
|
|
1996
|
+
}
|
|
1997
|
+
/** R3d — the turn's cells and its trouble, across every segment. */
|
|
1998
|
+
#turnCells(turn) {
|
|
1999
|
+
let n = 0;
|
|
2000
|
+
for (const seg of turn.segments)
|
|
2001
|
+
n += seg.cells.length;
|
|
2002
|
+
return n;
|
|
2003
|
+
}
|
|
2004
|
+
#turnHasTrouble(turn) {
|
|
2005
|
+
return turn.segments.some((seg) => this.#segmentHasTrouble(seg));
|
|
2006
|
+
}
|
|
1903
2007
|
/** R3b — how many cells the segment holds. The fold's threshold reads
|
|
1904
2008
|
* it; nothing else needs it, so it is counted rather than tracked. */
|
|
1905
2009
|
#segmentCells(seg) {
|
|
@@ -1948,9 +2052,15 @@ export class Body {
|
|
|
1948
2052
|
// The quiet turn is the same rule seen from one side: its single
|
|
1949
2053
|
// segment never closes until the settle, so it holds exactly as
|
|
1950
2054
|
// it always did.
|
|
2055
|
+
// R3d: the hold is the TURN's. A turn's work has no committed form
|
|
2056
|
+
// until the turn ends, because one line stands for all of it — and
|
|
2057
|
+
// a row already in the scrollback cannot be replaced by that line.
|
|
2058
|
+
// The force-commit path still overrides this (a turn too big for
|
|
2059
|
+
// the screen spills and renders normally); that is the honest
|
|
2060
|
+
// degradation, marked `spilled`.
|
|
1951
2061
|
const seg = this.#segmentOf(i);
|
|
1952
2062
|
if (seg !== null)
|
|
1953
|
-
return
|
|
2063
|
+
return !turn.ended;
|
|
1954
2064
|
// no segment (the pipe path's shape) — W14's original test, kept
|
|
1955
2065
|
// so a cell that never got a segment behaves as it used to.
|
|
1956
2066
|
if (!turn.ended && !turn.hasText)
|
|
@@ -2027,8 +2137,25 @@ export class Body {
|
|
|
2027
2137
|
// 0s · 1 shell` says strictly less than `shell make build ·
|
|
2028
2138
|
// exit 0`. The fold exists to stop a screen filling with work
|
|
2029
2139
|
// rows, and one row is not that.
|
|
2030
|
-
|
|
2031
|
-
|
|
2140
|
+
// R3d (owner, 2026-08-28) — a segment folds only on a QUIET turn.
|
|
2141
|
+
//
|
|
2142
|
+
// R3b folded every closed segment, and in use that was wrong for
|
|
2143
|
+
// a reason the design questions never surfaced: a model narrates
|
|
2144
|
+
// between calls, so a turn is not two or three segments, it is
|
|
2145
|
+
// one per tool. Every call became its own `✦ thought 2s · 1 read`
|
|
2146
|
+
// row — the same row count the fold exists to remove, now saying
|
|
2147
|
+
// less. The screen is not improved by summarising one thing.
|
|
2148
|
+
//
|
|
2149
|
+
// The turn's ONE line (renderRecap, R3d) carries the work now,
|
|
2150
|
+
// which is where it always belonged: it is already emitted once
|
|
2151
|
+
// per turn, in the right place, and it only needed to say what
|
|
2152
|
+
// the turn DID rather than "43 tools".
|
|
2153
|
+
//
|
|
2154
|
+
// The quiet turn keeps its fold because there IS no recap line
|
|
2155
|
+
// to carry it: a turn with no text is the fold, and W14's gates
|
|
2156
|
+
// pin that shape.
|
|
2157
|
+
if (turn !== undefined && seg !== null && seg.closedAt !== null && !this.#turnSpilled(turn) && turn.ended && this.#turnCells(turn) >= 2 && !this.#turnHasTrouble(turn)) {
|
|
2158
|
+
if (!turn.folded) {
|
|
2032
2159
|
seg.folded = true;
|
|
2033
2160
|
seg.headCell = i;
|
|
2034
2161
|
turn.folded = true;
|
|
@@ -2041,19 +2168,30 @@ export class Body {
|
|
|
2041
2168
|
// (turnFold is W-aware — the ONE row never trips
|
|
2042
2169
|
// invariant ①).
|
|
2043
2170
|
const quiet = turn.ended && !turn.hasText;
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
|
|
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.
|
|
2181
|
+
//
|
|
2182
|
+
// The terms are the TURN's, not the segment's: R3d folds
|
|
2183
|
+
// a turn's work into ONE line wherever the first work
|
|
2184
|
+
// lands. A per-segment line put a row on screen for every
|
|
2185
|
+
// break in the model's narration, which on a chatty model
|
|
2186
|
+
// is one row per tool — the row count the fold exists to
|
|
2187
|
+
// remove.
|
|
2188
|
+
const seconds = turn.thoughtSeconds;
|
|
2051
2189
|
return turnFold({
|
|
2052
2190
|
words: quiet ? turn.words : "",
|
|
2053
2191
|
thoughtSeconds: seconds,
|
|
2054
|
-
reads:
|
|
2055
|
-
edits:
|
|
2056
|
-
others: [...
|
|
2192
|
+
reads: turn.reads,
|
|
2193
|
+
edits: turn.edits,
|
|
2194
|
+
others: [...turn.others],
|
|
2057
2195
|
}, W);
|
|
2058
2196
|
}
|
|
2059
2197
|
return [];
|
|
@@ -2112,7 +2250,14 @@ export class Body {
|
|
|
2112
2250
|
// text's release they are — the natural loop commits the run in
|
|
2113
2251
|
// one frame; the force-commit's early commits degrade to the
|
|
2114
2252
|
// individual rows, the members render normally after).
|
|
2115
|
-
|
|
2253
|
+
// R3g (2026-08-28): ...and no member is in TROUBLE. A rollup
|
|
2254
|
+
// says "explored 3 paths" — a sentence a failed or interrupted
|
|
2255
|
+
// call makes false, and the row it replaces was the only place
|
|
2256
|
+
// that failure had words. Law 1.3 at the scale of a run: the
|
|
2257
|
+
// same rule #segmentHasTrouble applies to the fold. Found when
|
|
2258
|
+
// R3g's interrupt-closing made an aborted call `done`, which
|
|
2259
|
+
// let a run it never finished roll up as if it had.
|
|
2260
|
+
if (!members.every((c) => c.done && !c.isError && c.reason === null))
|
|
2116
2261
|
return cellComponent(cell).render(W, ctx);
|
|
2117
2262
|
this.#rolledHeads.add(head);
|
|
2118
2263
|
let total = 0;
|
|
@@ -2634,6 +2779,32 @@ export class Body {
|
|
|
2634
2779
|
/** Invariant ①: every emitted line fits the width — a violation is a
|
|
2635
2780
|
* CRASH with the diagnostic, never a silent truncate. */
|
|
2636
2781
|
#checked(line, W) {
|
|
2782
|
+
// Invariant ①b (R3f): a ROW IS ONE PHYSICAL ROW.
|
|
2783
|
+
//
|
|
2784
|
+
// The defect this catches shipped in 0.16.6 and smashed the
|
|
2785
|
+
// composer. `escapeTerminal` keeps `\n` (it strips C0 except tab
|
|
2786
|
+
// and newline), and `charWidth(0x0A)` is 1 — so a newline counts as
|
|
2787
|
+
// ONE CELL in `visibleWidth`, and every width check in the product,
|
|
2788
|
+
// invariant ① included, waves a multi-line string through as a
|
|
2789
|
+
// single row of legal width. `#emitDiff` then paints it as
|
|
2790
|
+
// `CUP(row,1) + EL + content`, the terminal's ONLCR moves the
|
|
2791
|
+
// cursor down at the newline, and the tail lands on whatever
|
|
2792
|
+
// physical row is there — the box rail, the input row. The diff
|
|
2793
|
+
// then adopts `desired` as the screen's truth, so the corruption
|
|
2794
|
+
// SURVIVES: the self-healing property this renderer is built on
|
|
2795
|
+
// ("a wrong row is repaired by the next frame, because the
|
|
2796
|
+
// difference includes it") is exactly what a lying `#screen`
|
|
2797
|
+
// breaks.
|
|
2798
|
+
//
|
|
2799
|
+
// Width was never the whole invariant — it was the half we
|
|
2800
|
+
// noticed. A row that occupies two physical rows violates the
|
|
2801
|
+
// geometry as surely as one that overruns the width, and it does
|
|
2802
|
+
// so INVISIBLY to a width check. `\r` is here for the same reason
|
|
2803
|
+
// (it moves the cursor to column 1).
|
|
2804
|
+
const bad = /[\n\r]/.exec(line);
|
|
2805
|
+
if (bad !== null) {
|
|
2806
|
+
throw new Error(`kiso-tui invariant ①b violated: a row containing ${JSON.stringify(bad[0])} was about to be emitted — a row must be ONE physical row, and the width check cannot see this (charWidth counts a newline as one cell) — ${JSON.stringify(line.slice(0, 80))}`);
|
|
2807
|
+
}
|
|
2637
2808
|
const w = visibleWidth(line);
|
|
2638
2809
|
if (w > W) {
|
|
2639
2810
|
throw new Error(`kiso-tui invariant ① violated: a line of visible width ${w} > ${W} was about to be emitted — ${JSON.stringify(line.slice(0, 80))}`);
|
package/dist/render.d.ts
CHANGED
|
@@ -144,15 +144,45 @@ export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio
|
|
|
144
144
|
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
145
145
|
*/
|
|
146
146
|
export interface RecapStats {
|
|
147
|
+
/** The TURN's wall seconds — what it took, start to settle. Named
|
|
148
|
+
* `took` on the row since R3g: it was labelled "thought" while the
|
|
149
|
+
* fold line one row above printed the kernel's MEASURED thinking
|
|
150
|
+
* seconds under that same word, so a turn that thought for 1s and
|
|
151
|
+
* then ran a 40s shell had two different numbers both called
|
|
152
|
+
* "thought". */
|
|
147
153
|
readonly seconds: number;
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
/**
|
|
155
|
+
* R3g — THE WORK TERMS ARE OPTIONAL, AND kiso NO LONGER PASSES THEM.
|
|
156
|
+
*
|
|
157
|
+
* The turn's work is said ONCE, by the compositor's fold line, in
|
|
158
|
+
* the place the work happened and with the key that reopens it. This
|
|
159
|
+
* row used to repeat it a few rows below — the same terms, a
|
|
160
|
+
* different clock — which is the doubling the owner called out.
|
|
161
|
+
*
|
|
162
|
+
* The fields stay, and still render when a caller supplies them, so
|
|
163
|
+
* an embedder of this package sees the byte-for-byte historical row.
|
|
164
|
+
* A turn whose work did NOT fold (it spilled past the live region,
|
|
165
|
+
* or it hit trouble) keeps every one of its rows on screen, so the
|
|
166
|
+
* work is not lost by their absence — it is standing right there.
|
|
167
|
+
*/
|
|
168
|
+
readonly tools?: number;
|
|
169
|
+
readonly edits?: number;
|
|
170
|
+
/** The turn's work BY TOOL, in first-call order (R3d). */
|
|
171
|
+
readonly byTool?: readonly [string, number][];
|
|
150
172
|
readonly usage: RunUsage;
|
|
151
173
|
/** R-C item 4: the per-turn cache miss (min(prevIn, in) − cacheRead),
|
|
152
174
|
* passed only when above the noise floor — the re-sent-uncached
|
|
153
175
|
* prefix. Absent → the recap bytes stay the historical form. */
|
|
154
176
|
readonly missed?: number;
|
|
155
177
|
readonly ctxLeftPct: number | null;
|
|
178
|
+
/** R3g — the terminal's width. The recap is the ONE row on the screen
|
|
179
|
+
* that was never measured: it is written raw, so a line longer than
|
|
180
|
+
* the terminal wrapped, and the second physical row is a fragment
|
|
181
|
+
* matching no cell format (the v2v lint reads it as interleaving).
|
|
182
|
+
* R3g's verb+noun terms made an 80-column wrap ordinary rather than
|
|
183
|
+
* rare, which is how it surfaced. Absent → uncut, the historical
|
|
184
|
+
* bytes, for the callers that render into no terminal. */
|
|
185
|
+
readonly width?: number;
|
|
156
186
|
/** W19 — the mode the turn ran under. Under "plan" the recap becomes
|
|
157
187
|
* the way-forward row (the claimed shape): a plan turn's currency is
|
|
158
188
|
* the plan, not the tool count — the timing and tool-count parts
|
package/dist/render.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* session line).
|
|
15
15
|
*/
|
|
16
16
|
import { escapeTerminal, foldResult, foldThinking, kUnit, palette } from "@vincemakes/kiso-tui-cells/render";
|
|
17
|
+
import { foldTerms, widthCut } from "@vincemakes/kiso-tui-cells/components";
|
|
18
|
+
import { visibleWidth } from "@vincemakes/kiso-tui-cells/width";
|
|
17
19
|
export * from "@vincemakes/kiso-tui-cells/render";
|
|
18
20
|
/**
|
|
19
21
|
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
@@ -163,6 +165,23 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
|
|
|
163
165
|
return null;
|
|
164
166
|
return `[turn ${turn} · ${parts.join(" · ")}]`;
|
|
165
167
|
}
|
|
168
|
+
/** R3d — the per-tool terms of a settled turn, in the fold line's own
|
|
169
|
+
* vocabulary (ROLLUP_NOUN plurals, zero terms dropped). One wording for
|
|
170
|
+
* "what a run did", wherever it is said. */
|
|
171
|
+
function recapWork(byTool) {
|
|
172
|
+
let reads = 0;
|
|
173
|
+
let edits = 0;
|
|
174
|
+
const others = [];
|
|
175
|
+
for (const [name, n] of byTool) {
|
|
176
|
+
if (name === "read_file")
|
|
177
|
+
reads += n;
|
|
178
|
+
else if (name === "edit_file")
|
|
179
|
+
edits += n;
|
|
180
|
+
else
|
|
181
|
+
others.push([name, n]);
|
|
182
|
+
}
|
|
183
|
+
return foldTerms(reads, edits, others);
|
|
184
|
+
}
|
|
166
185
|
export function renderRecap(s) {
|
|
167
186
|
const p = palette();
|
|
168
187
|
// W19: under plan the recap is the way out of the mode — the header
|
|
@@ -176,7 +195,21 @@ export function renderRecap(s) {
|
|
|
176
195
|
const parts = ["plan ready", "/mode default executes", "/mode accept-edits auto-approves edits"];
|
|
177
196
|
return `${p.bold}✦${p.reset} ${parts.join(" · ")}\n`;
|
|
178
197
|
}
|
|
179
|
-
|
|
198
|
+
// R3d (owner, 2026-08-28): the turn's ONE line says what the turn DID.
|
|
199
|
+
//
|
|
200
|
+
// The per-segment folds this replaces put a row on screen for every
|
|
201
|
+
// break in the model's narration — and a model that narrates between
|
|
202
|
+
// every call turned that into one row per tool, which is the row
|
|
203
|
+
// count the fold was built to remove, wearing a summary's clothes.
|
|
204
|
+
// The turn already had exactly one line in exactly the right place;
|
|
205
|
+
// it was just too coarse to be worth reading.
|
|
206
|
+
const edits = s.edits ?? 0;
|
|
207
|
+
const work = s.byTool !== undefined && s.byTool.length > 0
|
|
208
|
+
? recapWork(s.byTool)
|
|
209
|
+
: s.tools !== undefined && s.tools > 0
|
|
210
|
+
? [`${s.tools} tool${s.tools === 1 ? "" : "s"}${edits > 0 ? ` (${edits} edit${edits === 1 ? "" : "s"})` : ""}`]
|
|
211
|
+
: [];
|
|
212
|
+
const parts = [`took ${s.seconds}s`, ...work];
|
|
180
213
|
if (s.usage.known) {
|
|
181
214
|
const seg = `${s.usage.in !== null ? `in ${kUnit(s.usage.in)}` : ""}${s.usage.in !== null && s.usage.out !== null ? " " : ""}${s.usage.out !== null ? `out ${kUnit(s.usage.out)}` : ""}`;
|
|
182
215
|
if (seg !== "")
|
|
@@ -193,7 +226,17 @@ export function renderRecap(s) {
|
|
|
193
226
|
}
|
|
194
227
|
if (s.ctxLeftPct !== null)
|
|
195
228
|
parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
|
|
196
|
-
|
|
229
|
+
// R3g: ONE physical row, at any width — the same rule every other row
|
|
230
|
+
// in the product obeys. The cut is the honest "…": the recap said
|
|
231
|
+
// more than fits, and says so.
|
|
232
|
+
const line = parts.join(" · ");
|
|
233
|
+
// R3g: the floor is the renderer's own guard against a caller that
|
|
234
|
+
// hands it a degenerate width (a PTY with no winsize reports 0). A
|
|
235
|
+
// recap cut to one character is worse than one that wraps.
|
|
236
|
+
if (s.width !== undefined && s.width >= 20 && visibleWidth(`✦ ${line}`) > s.width) {
|
|
237
|
+
return `${p.bold}✦${p.reset} ${widthCut(line, Math.max(1, s.width - 3))}…\n`;
|
|
238
|
+
}
|
|
239
|
+
return `${p.bold}✦${p.reset} ${line}\n`;
|
|
197
240
|
}
|
|
198
241
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
199
242
|
export function renderSessionLine(meta) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.8",
|
|
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.16.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.16.8"
|
|
39
39
|
}
|
|
40
40
|
}
|