@vincemakes/kiso-tui 0.17.0 → 0.18.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.d.ts +35 -0
- package/dist/compositor.js +264 -42
- package/dist/editor.js +3 -0
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -184,6 +184,41 @@ export declare class Body {
|
|
|
184
184
|
isError: boolean;
|
|
185
185
|
};
|
|
186
186
|
} | null;
|
|
187
|
+
/**
|
|
188
|
+
* R4 (C4d) — THE APPEND-ONLY RE-WRAP.
|
|
189
|
+
*
|
|
190
|
+
* The owner's report: resize the window and the reference
|
|
191
|
+
* implementation's text re-wraps to the new width while kiso's does
|
|
192
|
+
* not. It is true, and it is not a bug to be fixed — it is the price
|
|
193
|
+
* of ADR-0046, and the price is worth naming precisely.
|
|
194
|
+
*
|
|
195
|
+
* A terminal can only reflow a SOFT-wrapped line: one long logical
|
|
196
|
+
* line the terminal itself wrapped as the cursor flowed past the last
|
|
197
|
+
* column. Every row kiso commits is either painted by cursor
|
|
198
|
+
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
199
|
+
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
200
|
+
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
201
|
+
* application hard-split. That same LF is what makes the transcript
|
|
202
|
+
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
203
|
+
* product whose transcript reflows is a product that repaints its
|
|
204
|
+
* transcript from its own memory, and that transcript dies with it.
|
|
205
|
+
*
|
|
206
|
+
* What kiso can do — and this is all it can do — is APPEND. The
|
|
207
|
+
* committed cells are still in memory; re-render them at the current
|
|
208
|
+
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
209
|
+
* above is rewritten, so ADR-0046 holds exactly.
|
|
210
|
+
*
|
|
211
|
+
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
212
|
+
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
213
|
+
* Tool rows, folds and chips are short, already carry their own
|
|
214
|
+
* width ladders, and re-printing them would duplicate work the folds
|
|
215
|
+
* exist to state once.
|
|
216
|
+
*/
|
|
217
|
+
rewrap(): {
|
|
218
|
+
lines: string[];
|
|
219
|
+
blocks: number;
|
|
220
|
+
skipped: number;
|
|
221
|
+
};
|
|
187
222
|
expandNext(): {
|
|
188
223
|
kind: "toggled";
|
|
189
224
|
} | {
|
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, stretchLine, turnFold, visibleWidth, twinkleFrame, } from "./components.js";
|
|
55
|
+
import { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, 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
|
|
@@ -179,7 +179,7 @@ function openSegment(turn, now) {
|
|
|
179
179
|
const last = turn.segments[turn.segments.length - 1];
|
|
180
180
|
if (last !== undefined && last.closedAt === null)
|
|
181
181
|
return last;
|
|
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: [] };
|
|
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, foldKey: null, cells: [] };
|
|
183
183
|
turn.segments.push(fresh);
|
|
184
184
|
return fresh;
|
|
185
185
|
}
|
|
@@ -314,7 +314,17 @@ export class Body {
|
|
|
314
314
|
// rendered row carried the "ctrl+r" affordance; the expand key's
|
|
315
315
|
// cycling pointer walks this list from the newest back.
|
|
316
316
|
#collapsed = [];
|
|
317
|
-
|
|
317
|
+
/** R4 (C1) — the ring walk is by IDENTITY, not by a modular pointer.
|
|
318
|
+
* `#collapsed` is unshifted on every commit that carries the key, so
|
|
319
|
+
* a numeric pointer's target silently CHANGED whenever a new fold
|
|
320
|
+
* landed mid-cycle: the ring was not stable under itself, and the
|
|
321
|
+
* next press opened something other than what the last press
|
|
322
|
+
* implied. This set records what the current cycle has already
|
|
323
|
+
* opened; the walk takes the newest entry not in it, and empties it
|
|
324
|
+
* when every entry has been seen. */
|
|
325
|
+
#opened = new Set();
|
|
326
|
+
/** R4 (C1) — the session's fold counter. Monotonic, never reused. */
|
|
327
|
+
#foldSeq = 0;
|
|
318
328
|
// W14: the turn records — one per userLine, the fold-hold's state
|
|
319
329
|
// machine (ended / hasText / folded) plus the folded-turn line's
|
|
320
330
|
// counts (accumulated at toolStart). The cells carry the record's
|
|
@@ -1024,6 +1034,73 @@ export class Body {
|
|
|
1024
1034
|
}
|
|
1025
1035
|
return -1;
|
|
1026
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* R4 (C4d) — THE APPEND-ONLY RE-WRAP.
|
|
1039
|
+
*
|
|
1040
|
+
* The owner's report: resize the window and the reference
|
|
1041
|
+
* implementation's text re-wraps to the new width while kiso's does
|
|
1042
|
+
* not. It is true, and it is not a bug to be fixed — it is the price
|
|
1043
|
+
* of ADR-0046, and the price is worth naming precisely.
|
|
1044
|
+
*
|
|
1045
|
+
* A terminal can only reflow a SOFT-wrapped line: one long logical
|
|
1046
|
+
* line the terminal itself wrapped as the cursor flowed past the last
|
|
1047
|
+
* column. Every row kiso commits is either painted by cursor
|
|
1048
|
+
* addressing (#emitDiff) or scrolled out by a bare LF (#emitScroll),
|
|
1049
|
+
* and frames run with autowrap OFF — so no byte kiso commits can ever
|
|
1050
|
+
* carry a continuation flag, and nothing downstream can rejoin rows an
|
|
1051
|
+
* application hard-split. That same LF is what makes the transcript
|
|
1052
|
+
* the TERMINAL's: it survives kiso's death, a pipe, and tmux. A
|
|
1053
|
+
* product whose transcript reflows is a product that repaints its
|
|
1054
|
+
* transcript from its own memory, and that transcript dies with it.
|
|
1055
|
+
*
|
|
1056
|
+
* What kiso can do — and this is all it can do — is APPEND. The
|
|
1057
|
+
* committed cells are still in memory; re-render them at the current
|
|
1058
|
+
* width and put them at the BOTTOM, where writing is allowed. Nothing
|
|
1059
|
+
* above is rewritten, so ADR-0046 holds exactly.
|
|
1060
|
+
*
|
|
1061
|
+
* Scoped to PROSE. Text is what reads badly at the wrong width — a
|
|
1062
|
+
* paragraph folded for 120 columns and read at 60 is the complaint.
|
|
1063
|
+
* Tool rows, folds and chips are short, already carry their own
|
|
1064
|
+
* width ladders, and re-printing them would duplicate work the folds
|
|
1065
|
+
* exist to state once.
|
|
1066
|
+
*/
|
|
1067
|
+
rewrap() {
|
|
1068
|
+
const W = this.#opts.width();
|
|
1069
|
+
const H = this.#opts.height();
|
|
1070
|
+
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
1071
|
+
// two screens is the bound: enough to re-read what a resize just
|
|
1072
|
+
// made awkward, short enough that the append is not its own wall
|
|
1073
|
+
// of text. A silent cap would read as "this is all of it".
|
|
1074
|
+
const budget = Math.max(H, 2 * H);
|
|
1075
|
+
const chunks = [];
|
|
1076
|
+
let rows = 0;
|
|
1077
|
+
let blocks = 0;
|
|
1078
|
+
let skipped = 0;
|
|
1079
|
+
for (let i = this.#committed - 1; i >= 0; i -= 1) {
|
|
1080
|
+
const cell = this.#cells[i];
|
|
1081
|
+
if (cell.kind !== "md")
|
|
1082
|
+
continue;
|
|
1083
|
+
blocks += 1;
|
|
1084
|
+
if (rows >= budget) {
|
|
1085
|
+
skipped += 1;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const lines = cellComponent(cell).render(W, ctx);
|
|
1089
|
+
chunks.unshift(lines);
|
|
1090
|
+
rows += lines.length;
|
|
1091
|
+
}
|
|
1092
|
+
return { lines: chunks.flat(), blocks: blocks - skipped, skipped };
|
|
1093
|
+
}
|
|
1094
|
+
/** R4 (C1) — what the NEXT press will open, by name. The walk is
|
|
1095
|
+
* deterministic (newest unopened first), so this is a promise the
|
|
1096
|
+
* key keeps rather than a guess. */
|
|
1097
|
+
#nextFoldHint() {
|
|
1098
|
+
const pending = this.#collapsed.filter((i) => !this.#opened.has(i));
|
|
1099
|
+
const ring = pending.length > 0 ? pending : this.#collapsed;
|
|
1100
|
+
const idx = ring[0];
|
|
1101
|
+
const key = idx === undefined ? null : (this.#segmentOf(idx)?.foldKey ?? null);
|
|
1102
|
+
return key === null ? "ctrl+r opens the next fold" : `ctrl+r opens fold ${key}`;
|
|
1103
|
+
}
|
|
1027
1104
|
expandNext() {
|
|
1028
1105
|
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
1029
1106
|
const cell = this.#cells[i];
|
|
@@ -1045,8 +1122,14 @@ export class Body {
|
|
|
1045
1122
|
}
|
|
1046
1123
|
if (this.#collapsed.length === 0)
|
|
1047
1124
|
return { kind: "none" };
|
|
1048
|
-
|
|
1049
|
-
|
|
1125
|
+
// R4 (C1) — the newest entry this cycle has not opened yet. When
|
|
1126
|
+
// every entry has been seen the cycle restarts, so the walk is
|
|
1127
|
+
// still "newest back" — it is simply immune to the ring growing
|
|
1128
|
+
// underneath it.
|
|
1129
|
+
if (this.#collapsed.every((i) => this.#opened.has(i)))
|
|
1130
|
+
this.#opened.clear();
|
|
1131
|
+
const idx = this.#collapsed.find((i) => !this.#opened.has(i)) ?? this.#collapsed[0];
|
|
1132
|
+
this.#opened.add(idx);
|
|
1050
1133
|
const cell = this.#cells[idx];
|
|
1051
1134
|
// R3b — a folded SEGMENT expands to the work it stands for.
|
|
1052
1135
|
//
|
|
@@ -1176,9 +1259,12 @@ export class Body {
|
|
|
1176
1259
|
return {
|
|
1177
1260
|
kind: "appended",
|
|
1178
1261
|
lines: [
|
|
1179
|
-
|
|
1262
|
+
// R4 (C1): the expansion names the fold it opened, in the
|
|
1263
|
+
// same ordinal the fold row printed — the answer to "which
|
|
1264
|
+
// one did that open", stated rather than inferred.
|
|
1265
|
+
`${p.bold}✦${p.reset} expanded${seg.foldKey === null ? "" : ` ${seg.foldKey}`} · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
|
|
1180
1266
|
...body,
|
|
1181
|
-
` ${p.dim}└ end of expansion ·
|
|
1267
|
+
` ${p.dim}└ end of expansion · ${this.#nextFoldHint()}${p.reset}`,
|
|
1182
1268
|
],
|
|
1183
1269
|
};
|
|
1184
1270
|
}
|
|
@@ -1680,7 +1766,21 @@ export class Body {
|
|
|
1680
1766
|
* settle still produces the same fold it did before. That is the
|
|
1681
1767
|
* charter's line between this phase and the next.
|
|
1682
1768
|
*/
|
|
1683
|
-
#liveProjection(W, ctx) {
|
|
1769
|
+
#liveProjection(W, ctx, cap) {
|
|
1770
|
+
const rows = this.#project(W, ctx, ACT_SLOT_ROWS);
|
|
1771
|
+
if (cap === undefined || rows.length <= cap)
|
|
1772
|
+
return rows;
|
|
1773
|
+
// R4 — the slot gives way BEFORE any cell is force-committed.
|
|
1774
|
+
// A standing slot that could overflow the content cap would make
|
|
1775
|
+
// the force-commit loop push REAL cells into the scrollback to
|
|
1776
|
+
// relieve rows that are, at the bottom of the slot, blank padding.
|
|
1777
|
+
// So the slot shrinks first, in the pinned order slotPad already
|
|
1778
|
+
// implements (the pad rows are last, so they go first, then the
|
|
1779
|
+
// tail, then the heads beyond the first) and the floor is one row.
|
|
1780
|
+
return this.#project(W, ctx, Math.max(1, ACT_SLOT_ROWS - (rows.length - cap)));
|
|
1781
|
+
}
|
|
1782
|
+
/** R4 — one pass of the live projection at a given slot budget. */
|
|
1783
|
+
#project(W, ctx, budget) {
|
|
1684
1784
|
const out = [];
|
|
1685
1785
|
const focus = this.#focusIndex();
|
|
1686
1786
|
const turn = this.#turns[this.#turns.length - 1];
|
|
@@ -1688,30 +1788,29 @@ export class Body {
|
|
|
1688
1788
|
const openSeg = open !== null && open.closedAt === null ? open : null;
|
|
1689
1789
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
1690
1790
|
let stretchDrawn = false;
|
|
1691
|
-
let runningShown = 0;
|
|
1692
|
-
let runningHidden = 0;
|
|
1693
1791
|
for (let i = this.#committed; i < this.#cells.length; i += 1) {
|
|
1694
1792
|
const cell = this.#cells[i];
|
|
1695
1793
|
const inOpen = openSeg !== null && openSeg.cells.includes(i);
|
|
1696
1794
|
if (inOpen) {
|
|
1697
|
-
// the stretch
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
//
|
|
1705
|
-
//
|
|
1706
|
-
|
|
1707
|
-
const flight = cell.kind === "tool" && !cell.done;
|
|
1708
|
-
if (!flight)
|
|
1795
|
+
// R4 — the open stretch is ONE contiguous block: its line
|
|
1796
|
+
// plus the standing act slot, spaced once, at the segment's
|
|
1797
|
+
// first live cell. Every other cell of the segment draws
|
|
1798
|
+
// nothing; its work is counted on the line and its output,
|
|
1799
|
+
// if it is the current thing, is in the slot.
|
|
1800
|
+
//
|
|
1801
|
+
// R3i drew the line here and then let each cell decide for
|
|
1802
|
+
// itself whether it still had rows — which is why the
|
|
1803
|
+
// region's height moved between every pair of calls.
|
|
1804
|
+
if (stretchDrawn)
|
|
1709
1805
|
continue;
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1806
|
+
stretchDrawn = true;
|
|
1807
|
+
const rows = [
|
|
1808
|
+
...stretchLine({ ...this.#stretchTerms(openSeg), liveNames: this.#liveNames(openSeg), phase: this.#stretchPhase(openSeg), mark: twinkleFrame(this.#spinnerI) }, W),
|
|
1809
|
+
...this.#actSlot(openSeg, W, ctx, budget, focus),
|
|
1810
|
+
];
|
|
1811
|
+
out.push(...this.#space(i, prev, rows));
|
|
1812
|
+
prev = rows;
|
|
1813
|
+
continue;
|
|
1715
1814
|
}
|
|
1716
1815
|
const rows = cellComponent(cell).render(W, ctx);
|
|
1717
1816
|
// the head row carries the affordance; the tint lands on it and
|
|
@@ -1721,12 +1820,120 @@ export class Body {
|
|
|
1721
1820
|
out.push(...this.#space(i, prev, rows));
|
|
1722
1821
|
prev = rows;
|
|
1723
1822
|
}
|
|
1724
|
-
if (runningHidden > 0) {
|
|
1725
|
-
const p = palette();
|
|
1726
|
-
out.push(` ${p.dim}└ +${runningHidden} more running${p.reset}`);
|
|
1727
|
-
}
|
|
1728
1823
|
return out;
|
|
1729
1824
|
}
|
|
1825
|
+
/**
|
|
1826
|
+
* R4 — the standing act slot's rows. EXACTLY `budget` rows in every
|
|
1827
|
+
* phase, so the live region's height changes twice per stretch (once
|
|
1828
|
+
* when it opens, once when it folds) instead of twice per call.
|
|
1829
|
+
*
|
|
1830
|
+
* The phases, in the order they are tested:
|
|
1831
|
+
* - an EXPANDED live cell outranks the slot (W15 — "the user asked
|
|
1832
|
+
* for it"): it renders in full, variable height. This is also
|
|
1833
|
+
* DC-28's cure: mid-stretch `ctrl+r` had a target it toggled and
|
|
1834
|
+
* never drew, so the press did nothing visible now and changed a
|
|
1835
|
+
* later expansion's shape;
|
|
1836
|
+
* - CALLS IN FLIGHT: one head row each within the budget, the tail
|
|
1837
|
+
* of the LAST head shown filling what is left, and the overflow
|
|
1838
|
+
* row inside the slot. The tail belongs to the last head by
|
|
1839
|
+
* construction — never call N's output under call N+1's header;
|
|
1840
|
+
* - the GAP between two calls: the call that just finished keeps its
|
|
1841
|
+
* settled head and its tail. This is the frame R3i collapsed, and
|
|
1842
|
+
* collapsing it is most of the jump;
|
|
1843
|
+
* - THINKING, before any call: the thinking's own tail (R3i ruling
|
|
1844
|
+
* 5, wired at last).
|
|
1845
|
+
*/
|
|
1846
|
+
#actSlot(seg, W, ctx, budget, focus) {
|
|
1847
|
+
const tint = (i, rows) => {
|
|
1848
|
+
if (i === focus && rows.length > 0)
|
|
1849
|
+
rows[0] = focusToken(rows[0], W);
|
|
1850
|
+
return rows;
|
|
1851
|
+
};
|
|
1852
|
+
const live = seg.cells.filter((i) => i >= this.#committed);
|
|
1853
|
+
const tools = [];
|
|
1854
|
+
for (const i of live)
|
|
1855
|
+
if (this.#cells[i]?.kind === "tool")
|
|
1856
|
+
tools.push(i);
|
|
1857
|
+
const toolAt = (i) => this.#cells[i];
|
|
1858
|
+
// An APPROVAL and an EXPANSION both outrank the slot, for the same
|
|
1859
|
+
// reason: their height is the human's business, not the renderer's.
|
|
1860
|
+
// W21 gives a pending approval the live region wholesale — its
|
|
1861
|
+
// diff is the thing being decided about, and a diff clamped to
|
|
1862
|
+
// four rows is a decision made on partial evidence. W15 gives an
|
|
1863
|
+
// expanded cell its full body — "the user asked for it". The slot
|
|
1864
|
+
// exists to stop the height moving ON ITS OWN; a height a human
|
|
1865
|
+
// asked for is not the oscillation it was built against.
|
|
1866
|
+
//
|
|
1867
|
+
// (The approval half is a regression this round caused and its
|
|
1868
|
+
// gate caught: the first draft treated a pending approval as a
|
|
1869
|
+
// call in flight, so `⏸ edit x.ts` lost its diff tail and the
|
|
1870
|
+
// `ctrl+r to expand` note with it.)
|
|
1871
|
+
const owned = tools.filter((i) => toolAt(i).expanded || toolAt(i).state === "approval");
|
|
1872
|
+
if (owned.length > 0) {
|
|
1873
|
+
// In CELL ORDER, so the frame reads the way the work happened:
|
|
1874
|
+
// an owned cell in full, every OTHER call still in flight
|
|
1875
|
+
// keeping its head row. An approval pausing one call must never
|
|
1876
|
+
// hide the others — the v2d parallel-frame gate caught exactly
|
|
1877
|
+
// that: with the shell running and asky_read at its panel, the
|
|
1878
|
+
// first draft returned the panel alone and the running shell's
|
|
1879
|
+
// `● shell sleep 1; echo hi · 1s` row vanished from the screen.
|
|
1880
|
+
const shown = tools.filter((i) => owned.includes(i) || !toolAt(i).done);
|
|
1881
|
+
const out = [];
|
|
1882
|
+
let heads = 0;
|
|
1883
|
+
for (const i of shown) {
|
|
1884
|
+
const rows = tint(i, cellComponent(this.#cells[i]).render(W, ctx));
|
|
1885
|
+
if (owned.includes(i)) {
|
|
1886
|
+
out.push(...rows);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
if (heads >= LIVE_ACT_HEADS)
|
|
1890
|
+
continue;
|
|
1891
|
+
heads += 1;
|
|
1892
|
+
out.push(rows[0] ?? "");
|
|
1893
|
+
}
|
|
1894
|
+
const hidden = shown.length - owned.length - heads;
|
|
1895
|
+
if (hidden > 0)
|
|
1896
|
+
out.push(moreRunningRow(hidden, W));
|
|
1897
|
+
return out;
|
|
1898
|
+
}
|
|
1899
|
+
const flight = tools.filter((i) => !toolAt(i).done);
|
|
1900
|
+
if (flight.length > 0) {
|
|
1901
|
+
// the commonest frame — exactly one call, the full budget — is
|
|
1902
|
+
// the W8 block verbatim, which is what 0.17.0 already drew.
|
|
1903
|
+
if (flight.length === 1 && budget >= ACT_SLOT_ROWS)
|
|
1904
|
+
return slotPad(tint(flight[0], cellComponent(this.#cells[flight[0]]).render(W, ctx)), budget);
|
|
1905
|
+
const heads = flight.slice(0, Math.max(1, Math.min(flight.length, budget - 1, LIVE_ACT_HEADS)));
|
|
1906
|
+
const hidden = flight.length - heads.length;
|
|
1907
|
+
const rows = [];
|
|
1908
|
+
for (const i of heads)
|
|
1909
|
+
rows.push(tint(i, cellComponent(this.#cells[i]).render(W, ctx))[0] ?? "");
|
|
1910
|
+
const rest = budget - rows.length - (hidden > 0 ? 1 : 0);
|
|
1911
|
+
if (rest > 0)
|
|
1912
|
+
rows.push(...slotTail(toolAt(heads[heads.length - 1]).resultText, W, rest));
|
|
1913
|
+
if (hidden > 0)
|
|
1914
|
+
rows.push(moreRunningRow(hidden, W));
|
|
1915
|
+
return slotPad(rows, budget);
|
|
1916
|
+
}
|
|
1917
|
+
const settled = tools.length > 0 ? tools[tools.length - 1] : null;
|
|
1918
|
+
if (settled !== null) {
|
|
1919
|
+
const head = tint(settled, cellComponent(this.#cells[settled]).render(W, ctx))[0] ?? "";
|
|
1920
|
+
return slotPad([head, ...slotTail(toolAt(settled).resultText, W, budget - 1)], budget);
|
|
1921
|
+
}
|
|
1922
|
+
const think = [...live].reverse().find((i) => this.#cells[i]?.kind === "thinking");
|
|
1923
|
+
return slotPad(think === undefined ? [] : slotTail(this.#cells[think].text, W, budget), budget);
|
|
1924
|
+
}
|
|
1925
|
+
/** R4 — the tool names with a call still IN FLIGHT in this segment.
|
|
1926
|
+
* The stretch line's tense is per term, so a finished shell reads
|
|
1927
|
+
* `ran 1 shell command` while a read is still running. */
|
|
1928
|
+
#liveNames(seg) {
|
|
1929
|
+
const names = new Set();
|
|
1930
|
+
for (const i of seg.cells) {
|
|
1931
|
+
const c = this.#cells[i];
|
|
1932
|
+
if (c !== undefined && c.kind === "tool" && !c.done)
|
|
1933
|
+
names.add(c.name);
|
|
1934
|
+
}
|
|
1935
|
+
return [...names];
|
|
1936
|
+
}
|
|
1730
1937
|
/** R3i — the open stretch's phase. It is THINKING while a thinking
|
|
1731
1938
|
* cell of it is still open and no call has started; otherwise it is
|
|
1732
1939
|
* ACTING. The tense follows the phase, and the phase is what the
|
|
@@ -1796,16 +2003,25 @@ export class Body {
|
|
|
1796
2003
|
inputExtra +
|
|
1797
2004
|
queueRows.length);
|
|
1798
2005
|
}
|
|
2006
|
+
// DC-27 — the scalar measures the PROJECTION, not a second render
|
|
2007
|
+
// of its own. This loop used to walk every live cell and render it
|
|
2008
|
+
// in full: no open-segment collapse, no flight rule, no act-slot
|
|
2009
|
+
// budget. After R3i that described a screen the compositor had
|
|
2010
|
+
// stopped drawing — for an open stretch with five finished calls
|
|
2011
|
+
// it counted five four-row blocks that were not there. Nothing
|
|
2012
|
+
// broke, because the force-commit loop measures liveLines.length
|
|
2013
|
+
// and the over-count is conservative; but the cap and geometry
|
|
2014
|
+
// gates were asserting a property of a function nothing paints
|
|
2015
|
+
// from, so a real regression in the region's height could not
|
|
2016
|
+
// have moved them. The rule this file already states for the
|
|
2017
|
+
// sheet ("the scalar must say so, or the cap arithmetic disagrees
|
|
2018
|
+
// with the screen") is the same rule here.
|
|
1799
2019
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
|
|
1800
2020
|
const W = this.#opts.width();
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
lines += this.#space(i, prev, rows).length;
|
|
1806
|
-
prev = rows;
|
|
1807
|
-
}
|
|
1808
|
-
return lines + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
|
|
2021
|
+
// the SAME content cap the force-commit loop applies, so the
|
|
2022
|
+
// scalar sees the same slot budget the screen gets.
|
|
2023
|
+
const rows = this.#liveProjection(W, ctx, this.#opts.height() - 4 - inputExtra - queueRows.length);
|
|
2024
|
+
return rows.length + CHROME_ROWS + inputExtra + this.#menuRows(W).length + queueRows.length;
|
|
1809
2025
|
}
|
|
1810
2026
|
/** The lines committed THIS frame — the writes land in the frame's
|
|
1811
2027
|
* committed section (the rows just above the live region). */
|
|
@@ -1914,7 +2130,7 @@ export class Body {
|
|
|
1914
2130
|
// by construction), so the marker can never point at a cell the
|
|
1915
2131
|
// key would not take — which is the only way a focus marker is
|
|
1916
2132
|
// worth having.
|
|
1917
|
-
liveLines = this.#liveProjection(W, ctx);
|
|
2133
|
+
liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
|
|
1918
2134
|
}
|
|
1919
2135
|
// 3. the FORCE commits — the live region's hard cap H−1: overflow
|
|
1920
2136
|
// commits the oldest live cell UNCONDITIONALLY (the one sharp
|
|
@@ -1935,7 +2151,7 @@ export class Body {
|
|
|
1935
2151
|
this.#commitCell(this.#committed, W, ctx);
|
|
1936
2152
|
// TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
|
|
1937
2153
|
// pointed at may have just left the live region.
|
|
1938
|
-
liveLines = this.#liveProjection(W, ctx);
|
|
2154
|
+
liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
|
|
1939
2155
|
}
|
|
1940
2156
|
// 4. the geometry — the live region's first row:
|
|
1941
2157
|
// liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
|
|
@@ -2450,6 +2666,12 @@ export class Body {
|
|
|
2450
2666
|
seg.folded = true;
|
|
2451
2667
|
seg.headCell = i;
|
|
2452
2668
|
turn.folded = true;
|
|
2669
|
+
// R4 (C1): the ordinal is assigned HERE, before the row
|
|
2670
|
+
// is rendered, because the settled line always carries
|
|
2671
|
+
// the key — so the number is known without guessing
|
|
2672
|
+
// whether the row will earn an affordance.
|
|
2673
|
+
this.#foldSeq += 1;
|
|
2674
|
+
seg.foldKey = this.#foldSeq;
|
|
2453
2675
|
// DECLARED SUPERSESSION (R3i phase 3) — A9 NARROWS: the
|
|
2454
2676
|
// fold carries WORK, never the human's words.
|
|
2455
2677
|
//
|
|
@@ -2461,7 +2683,7 @@ export class Body {
|
|
|
2461
2683
|
// the fold directly beneath it. The band is the record of
|
|
2462
2684
|
// what was asked; this line is the record of what was
|
|
2463
2685
|
// done. One fact, one row, each.
|
|
2464
|
-
return stretchLine({ ...this.#stretchTerms(seg), phase: "settled" }, W);
|
|
2686
|
+
return stretchLine({ ...this.#stretchTerms(seg), phase: "settled", foldKey: seg.foldKey }, W);
|
|
2465
2687
|
}
|
|
2466
2688
|
return [];
|
|
2467
2689
|
}
|
package/dist/editor.js
CHANGED
|
@@ -61,6 +61,9 @@ export const MENU_ITEMS = [
|
|
|
61
61
|
{ name: "/resume", desc: "switch to another session; /resume <id> goes directly" },
|
|
62
62
|
{ name: "/think", desc: "show the last full thinking block" },
|
|
63
63
|
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
64
|
+
// R4 (C4d): the committed transcript belongs to the terminal and can
|
|
65
|
+
// never be re-wrapped in place (ADR-0046); this appends it re-folded.
|
|
66
|
+
{ name: "/rewrap", desc: "re-print the recent prose at the current width" },
|
|
64
67
|
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
65
68
|
// TUI2-R1 (E): the rent-ledger attribution — where the context went
|
|
66
69
|
{ name: "/context", desc: "show where the context went — the last request's rent ledger" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.18.0"
|
|
39
39
|
}
|
|
40
40
|
}
|