@vincemakes/kiso-tui 0.16.6 → 0.16.7
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 +164 -18
- package/dist/render.d.ts +32 -2
- package/dist/render.js +45 -2
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -765,6 +765,25 @@ export class Body {
|
|
|
765
765
|
break;
|
|
766
766
|
}
|
|
767
767
|
}
|
|
768
|
+
// R3g (fable D3, 2026-08-28): an INTERRUPTED tool never receives a
|
|
769
|
+
// result, so its cell stays `done: false` — and the commit loop
|
|
770
|
+
// stops at the first cell that is not done. One esc mid-tool
|
|
771
|
+
// therefore parked the commit pointer for the REST of the
|
|
772
|
+
// session: every later turn's rows piled up in the live region
|
|
773
|
+
// and only ever left it through the force-commit cap. The turn's
|
|
774
|
+
// end is the boundary that closes them, exactly as it closes an
|
|
775
|
+
// open thinking cell. `reason` is set so the row keeps its words
|
|
776
|
+
// AND so #segmentHasTrouble holds the turn unfolded — an
|
|
777
|
+
// interruption is trouble, and law 1.3 says trouble is never
|
|
778
|
+
// summarised away.
|
|
779
|
+
for (const c of this.#cells) {
|
|
780
|
+
if (c.kind === "tool" && !c.done) {
|
|
781
|
+
c.state = "done";
|
|
782
|
+
c.reason = "interrupted";
|
|
783
|
+
c.doneAt = Date.now();
|
|
784
|
+
c.done = true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
768
787
|
// the QUIET turn: an open thinking cell closes at the boundary —
|
|
769
788
|
// its natural closer is the text's arrival (never comes here — the
|
|
770
789
|
// text-less turn), so without this the fold could never commit AT
|
|
@@ -963,7 +982,8 @@ export class Body {
|
|
|
963
982
|
// expand in this method does, and they are the cells' OWN renders,
|
|
964
983
|
// so the expansion cannot drift from what was folded.
|
|
965
984
|
const seg = this.#segmentOf(idx);
|
|
966
|
-
|
|
985
|
+
const foldTurn = (cell.kind === "thinking" || cell.kind === "tool") && cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
|
|
986
|
+
if (seg !== null && foldTurn !== undefined && seg.headCell === idx) {
|
|
967
987
|
const p = palette();
|
|
968
988
|
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
969
989
|
const back = `${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
|
|
@@ -1015,7 +1035,22 @@ export class Body {
|
|
|
1015
1035
|
}
|
|
1016
1036
|
run = [];
|
|
1017
1037
|
};
|
|
1018
|
-
|
|
1038
|
+
// R3f — the expansion covers the WHOLE TURN, every segment.
|
|
1039
|
+
//
|
|
1040
|
+
// R3d moved the fold to the turn while the expansion kept
|
|
1041
|
+
// walking one segment, so a turn that spoke between calls
|
|
1042
|
+
// folded to a line claiming `3 reads · 1 edit · 1 shell` whose
|
|
1043
|
+
// key opened only the reads: the edit and the shell were on no
|
|
1044
|
+
// surface and reachable by no key. That is the one thing this
|
|
1045
|
+
// round's own first gate forbids — the work is never
|
|
1046
|
+
// unreachable — and it is worse than never folding, because the
|
|
1047
|
+
// line names work it then withholds.
|
|
1048
|
+
//
|
|
1049
|
+
// A run still BREAKS at a non-explore cell, so the segment
|
|
1050
|
+
// boundaries survive where they carry meaning (the write that
|
|
1051
|
+
// splits two explore runs); they simply no longer bound what
|
|
1052
|
+
// the key can reach.
|
|
1053
|
+
for (const j of foldTurn.segments.flatMap((sg) => sg.cells).sort((a, b) => a - b)) {
|
|
1019
1054
|
if (j < idx)
|
|
1020
1055
|
continue;
|
|
1021
1056
|
const c = this.#cells[j];
|
|
@@ -1035,7 +1070,11 @@ export class Body {
|
|
|
1035
1070
|
// terms; each run below states what IT did, in the rollup's. Two
|
|
1036
1071
|
// scales, one wording each — the header used to borrow the run's
|
|
1037
1072
|
// sentence, which read as the same run twice.
|
|
1038
|
-
|
|
1073
|
+
// the header names what the FOLD said — the turn's terms — so the
|
|
1074
|
+
// line you pressed and the block it opens agree. It used to name
|
|
1075
|
+
// segment 1's, which contradicted the fold above it.
|
|
1076
|
+
const head = foldTerms(foldTurn.reads, foldTurn.edits, [...foldTurn.others]);
|
|
1077
|
+
return { kind: "appended", lines: [`${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`, ...rows] };
|
|
1039
1078
|
}
|
|
1040
1079
|
if (cell.kind !== "tool")
|
|
1041
1080
|
return { kind: "none" };
|
|
@@ -1675,6 +1714,17 @@ export class Body {
|
|
|
1675
1714
|
// edge — the cap scalar is asserted by the gates). W22: the
|
|
1676
1715
|
// queue band shrinks the cap by its rows (empty queue → H−4).
|
|
1677
1716
|
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)
|
|
1717
|
+
// R3f: the cell about to be force-committed marks its segment
|
|
1718
|
+
// SPILLED. The rule was written at R3b — "a segment too big for
|
|
1719
|
+
// the screen already has rows in the scrollback that cannot be
|
|
1720
|
+
// taken back, so it renders normally and does not collapse" —
|
|
1721
|
+
// and then never wired: `spilled` had a declaration, an
|
|
1722
|
+
// initializer and a read, and nothing ever set it. The read was
|
|
1723
|
+
// therefore vacuously true, so a 43-call turn force-committed
|
|
1724
|
+
// thirty expanded rows and STILL printed `✦ thought 103s · 43
|
|
1725
|
+
// reads` underneath them, claiming as folded the work standing
|
|
1726
|
+
// visible above it.
|
|
1727
|
+
this.#markSpilled(this.#committed);
|
|
1678
1728
|
this.#commitCell(this.#committed, W, ctx);
|
|
1679
1729
|
liveLines = [];
|
|
1680
1730
|
{
|
|
@@ -1888,7 +1938,12 @@ export class Body {
|
|
|
1888
1938
|
* `✦ thought 3s · 20 reads` while a write was refused inside it.
|
|
1889
1939
|
*/
|
|
1890
1940
|
#segmentHasTrouble(seg) {
|
|
1891
|
-
|
|
1941
|
+
// R3g (fable, 2026-08-28): a DENIED call is the case this rule
|
|
1942
|
+
// exists for, and it was the one case the predicate could not
|
|
1943
|
+
// see — a denial carrying no `reason` string leaves isError
|
|
1944
|
+
// false and reason null, so `✦ thought 3s · 20 reads` could
|
|
1945
|
+
// stand over a refused write. The verdict is the record of it.
|
|
1946
|
+
return this.#segmentTools(seg).some((c) => c.isError || c.reason !== null || c.verdict?.decision === "denied");
|
|
1892
1947
|
}
|
|
1893
1948
|
/** R3b — the segment's TOOL cells, in order. */
|
|
1894
1949
|
#segmentTools(seg) {
|
|
@@ -1900,6 +1955,30 @@ export class Body {
|
|
|
1900
1955
|
}
|
|
1901
1956
|
return out;
|
|
1902
1957
|
}
|
|
1958
|
+
/** R3f — the cell is leaving the live region under the screen's hard
|
|
1959
|
+
* cap, so its segment can no longer be represented by a fold. */
|
|
1960
|
+
#markSpilled(i) {
|
|
1961
|
+
const seg = this.#segmentOf(i);
|
|
1962
|
+
if (seg !== null)
|
|
1963
|
+
seg.spilled = true;
|
|
1964
|
+
}
|
|
1965
|
+
/** R3f — did ANY of the turn's segments spill? The fold is the
|
|
1966
|
+
* TURN's, so one spilled segment makes the whole turn unfoldable:
|
|
1967
|
+
* a line claiming the turn's counts cannot stand under rows that
|
|
1968
|
+
* already show part of that same work. */
|
|
1969
|
+
#turnSpilled(turn) {
|
|
1970
|
+
return turn.segments.some((seg) => seg.spilled);
|
|
1971
|
+
}
|
|
1972
|
+
/** R3d — the turn's cells and its trouble, across every segment. */
|
|
1973
|
+
#turnCells(turn) {
|
|
1974
|
+
let n = 0;
|
|
1975
|
+
for (const seg of turn.segments)
|
|
1976
|
+
n += seg.cells.length;
|
|
1977
|
+
return n;
|
|
1978
|
+
}
|
|
1979
|
+
#turnHasTrouble(turn) {
|
|
1980
|
+
return turn.segments.some((seg) => this.#segmentHasTrouble(seg));
|
|
1981
|
+
}
|
|
1903
1982
|
/** R3b — how many cells the segment holds. The fold's threshold reads
|
|
1904
1983
|
* it; nothing else needs it, so it is counted rather than tracked. */
|
|
1905
1984
|
#segmentCells(seg) {
|
|
@@ -1948,9 +2027,15 @@ export class Body {
|
|
|
1948
2027
|
// The quiet turn is the same rule seen from one side: its single
|
|
1949
2028
|
// segment never closes until the settle, so it holds exactly as
|
|
1950
2029
|
// it always did.
|
|
2030
|
+
// R3d: the hold is the TURN's. A turn's work has no committed form
|
|
2031
|
+
// until the turn ends, because one line stands for all of it — and
|
|
2032
|
+
// a row already in the scrollback cannot be replaced by that line.
|
|
2033
|
+
// The force-commit path still overrides this (a turn too big for
|
|
2034
|
+
// the screen spills and renders normally); that is the honest
|
|
2035
|
+
// degradation, marked `spilled`.
|
|
1951
2036
|
const seg = this.#segmentOf(i);
|
|
1952
2037
|
if (seg !== null)
|
|
1953
|
-
return
|
|
2038
|
+
return !turn.ended;
|
|
1954
2039
|
// no segment (the pipe path's shape) — W14's original test, kept
|
|
1955
2040
|
// so a cell that never got a segment behaves as it used to.
|
|
1956
2041
|
if (!turn.ended && !turn.hasText)
|
|
@@ -2027,8 +2112,25 @@ export class Body {
|
|
|
2027
2112
|
// 0s · 1 shell` says strictly less than `shell make build ·
|
|
2028
2113
|
// exit 0`. The fold exists to stop a screen filling with work
|
|
2029
2114
|
// rows, and one row is not that.
|
|
2030
|
-
|
|
2031
|
-
|
|
2115
|
+
// R3d (owner, 2026-08-28) — a segment folds only on a QUIET turn.
|
|
2116
|
+
//
|
|
2117
|
+
// R3b folded every closed segment, and in use that was wrong for
|
|
2118
|
+
// a reason the design questions never surfaced: a model narrates
|
|
2119
|
+
// between calls, so a turn is not two or three segments, it is
|
|
2120
|
+
// one per tool. Every call became its own `✦ thought 2s · 1 read`
|
|
2121
|
+
// row — the same row count the fold exists to remove, now saying
|
|
2122
|
+
// less. The screen is not improved by summarising one thing.
|
|
2123
|
+
//
|
|
2124
|
+
// The turn's ONE line (renderRecap, R3d) carries the work now,
|
|
2125
|
+
// which is where it always belonged: it is already emitted once
|
|
2126
|
+
// per turn, in the right place, and it only needed to say what
|
|
2127
|
+
// the turn DID rather than "43 tools".
|
|
2128
|
+
//
|
|
2129
|
+
// The quiet turn keeps its fold because there IS no recap line
|
|
2130
|
+
// to carry it: a turn with no text is the fold, and W14's gates
|
|
2131
|
+
// pin that shape.
|
|
2132
|
+
if (turn !== undefined && seg !== null && seg.closedAt !== null && !this.#turnSpilled(turn) && turn.ended && this.#turnCells(turn) >= 2 && !this.#turnHasTrouble(turn)) {
|
|
2133
|
+
if (!turn.folded) {
|
|
2032
2134
|
seg.folded = true;
|
|
2033
2135
|
seg.headCell = i;
|
|
2034
2136
|
turn.folded = true;
|
|
@@ -2041,19 +2143,30 @@ export class Body {
|
|
|
2041
2143
|
// (turnFold is W-aware — the ONE row never trips
|
|
2042
2144
|
// invariant ①).
|
|
2043
2145
|
const quiet = turn.ended && !turn.hasText;
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
|
|
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.
|
|
2156
|
+
//
|
|
2157
|
+
// The terms are the TURN's, not the segment's: R3d folds
|
|
2158
|
+
// a turn's work into ONE line wherever the first work
|
|
2159
|
+
// lands. A per-segment line put a row on screen for every
|
|
2160
|
+
// break in the model's narration, which on a chatty model
|
|
2161
|
+
// is one row per tool — the row count the fold exists to
|
|
2162
|
+
// remove.
|
|
2163
|
+
const seconds = turn.thoughtSeconds;
|
|
2051
2164
|
return turnFold({
|
|
2052
2165
|
words: quiet ? turn.words : "",
|
|
2053
2166
|
thoughtSeconds: seconds,
|
|
2054
|
-
reads:
|
|
2055
|
-
edits:
|
|
2056
|
-
others: [...
|
|
2167
|
+
reads: turn.reads,
|
|
2168
|
+
edits: turn.edits,
|
|
2169
|
+
others: [...turn.others],
|
|
2057
2170
|
}, W);
|
|
2058
2171
|
}
|
|
2059
2172
|
return [];
|
|
@@ -2112,7 +2225,14 @@ export class Body {
|
|
|
2112
2225
|
// text's release they are — the natural loop commits the run in
|
|
2113
2226
|
// one frame; the force-commit's early commits degrade to the
|
|
2114
2227
|
// individual rows, the members render normally after).
|
|
2115
|
-
|
|
2228
|
+
// R3g (2026-08-28): ...and no member is in TROUBLE. A rollup
|
|
2229
|
+
// says "explored 3 paths" — a sentence a failed or interrupted
|
|
2230
|
+
// call makes false, and the row it replaces was the only place
|
|
2231
|
+
// that failure had words. Law 1.3 at the scale of a run: the
|
|
2232
|
+
// same rule #segmentHasTrouble applies to the fold. Found when
|
|
2233
|
+
// R3g's interrupt-closing made an aborted call `done`, which
|
|
2234
|
+
// let a run it never finished roll up as if it had.
|
|
2235
|
+
if (!members.every((c) => c.done && !c.isError && c.reason === null))
|
|
2116
2236
|
return cellComponent(cell).render(W, ctx);
|
|
2117
2237
|
this.#rolledHeads.add(head);
|
|
2118
2238
|
let total = 0;
|
|
@@ -2634,6 +2754,32 @@ export class Body {
|
|
|
2634
2754
|
/** Invariant ①: every emitted line fits the width — a violation is a
|
|
2635
2755
|
* CRASH with the diagnostic, never a silent truncate. */
|
|
2636
2756
|
#checked(line, W) {
|
|
2757
|
+
// Invariant ①b (R3f): a ROW IS ONE PHYSICAL ROW.
|
|
2758
|
+
//
|
|
2759
|
+
// The defect this catches shipped in 0.16.6 and smashed the
|
|
2760
|
+
// composer. `escapeTerminal` keeps `\n` (it strips C0 except tab
|
|
2761
|
+
// and newline), and `charWidth(0x0A)` is 1 — so a newline counts as
|
|
2762
|
+
// ONE CELL in `visibleWidth`, and every width check in the product,
|
|
2763
|
+
// invariant ① included, waves a multi-line string through as a
|
|
2764
|
+
// single row of legal width. `#emitDiff` then paints it as
|
|
2765
|
+
// `CUP(row,1) + EL + content`, the terminal's ONLCR moves the
|
|
2766
|
+
// cursor down at the newline, and the tail lands on whatever
|
|
2767
|
+
// physical row is there — the box rail, the input row. The diff
|
|
2768
|
+
// then adopts `desired` as the screen's truth, so the corruption
|
|
2769
|
+
// SURVIVES: the self-healing property this renderer is built on
|
|
2770
|
+
// ("a wrong row is repaired by the next frame, because the
|
|
2771
|
+
// difference includes it") is exactly what a lying `#screen`
|
|
2772
|
+
// breaks.
|
|
2773
|
+
//
|
|
2774
|
+
// Width was never the whole invariant — it was the half we
|
|
2775
|
+
// noticed. A row that occupies two physical rows violates the
|
|
2776
|
+
// geometry as surely as one that overruns the width, and it does
|
|
2777
|
+
// so INVISIBLY to a width check. `\r` is here for the same reason
|
|
2778
|
+
// (it moves the cursor to column 1).
|
|
2779
|
+
const bad = /[\n\r]/.exec(line);
|
|
2780
|
+
if (bad !== null) {
|
|
2781
|
+
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))}`);
|
|
2782
|
+
}
|
|
2637
2783
|
const w = visibleWidth(line);
|
|
2638
2784
|
if (w > W) {
|
|
2639
2785
|
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.7",
|
|
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.7"
|
|
39
39
|
}
|
|
40
40
|
}
|