@vincemakes/kiso-tui 0.24.4 → 0.25.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 +18 -25
- package/dist/compositor.js +201 -152
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -227,31 +227,6 @@ export declare class Body {
|
|
|
227
227
|
* there is no pointer, so there is nothing a pointer could reach
|
|
228
228
|
* that a keyboard cannot. */
|
|
229
229
|
viewerKey(cmd: "up" | "down" | "toggle" | "all" | "pageUp" | "pageDown" | "home" | "end"): void;
|
|
230
|
-
/**
|
|
231
|
-
* DC-35 — ctrl+o does not print the same expansion twice in a row.
|
|
232
|
-
*
|
|
233
|
-
* The ring walks newest-back and restarts its cycle once every entry
|
|
234
|
-
* has been opened (R4/C1, which is what makes the walk immune to the
|
|
235
|
-
* ring growing underneath it). With a ring of ONE the restart is
|
|
236
|
-
* immediate, so holding the key appended the identical block over
|
|
237
|
-
* and over — the owner got three copies of the same four rows, each
|
|
238
|
-
* closing with `ctrl+o opens the one before it`, a footer naming
|
|
239
|
-
* something that does not exist.
|
|
240
|
-
*
|
|
241
|
-
* The bar is the BOTTOM of the transcript, not "ever shown": once
|
|
242
|
-
* other content has arrived the expansion has scrolled up and
|
|
243
|
-
* re-opening it is the point of the key, so the guard clears itself
|
|
244
|
-
* the moment a cell is added.
|
|
245
|
-
*/
|
|
246
|
-
expandNext(): {
|
|
247
|
-
kind: "toggled";
|
|
248
|
-
} | {
|
|
249
|
-
kind: "appended";
|
|
250
|
-
lines: string[];
|
|
251
|
-
} | {
|
|
252
|
-
kind: "none";
|
|
253
|
-
why?: "already-last";
|
|
254
|
-
};
|
|
255
230
|
/** Docked = the chrome is live (a color TTY with a real size). */
|
|
256
231
|
get active(): boolean;
|
|
257
232
|
enter(): void;
|
|
@@ -313,6 +288,24 @@ export declare class Body {
|
|
|
313
288
|
* repaint, once per session.
|
|
314
289
|
*/
|
|
315
290
|
onGroundChange(): void;
|
|
291
|
+
/**
|
|
292
|
+
* DC-50 — ctrl+o flips ONE switch, and every settled card obeys it.
|
|
293
|
+
*
|
|
294
|
+
* The walk this replaces existed because ADR-0046 §3 forbade
|
|
295
|
+
* re-rendering a committed card, so the only way to show one's body
|
|
296
|
+
* was to APPEND a copy further down — with a ring to decide which
|
|
297
|
+
* card was next, `#opened` to keep the ring from repeating, and
|
|
298
|
+
* `#lastAppend` to stop a held key printing the same rows three times
|
|
299
|
+
* (DC-35). Amendment 1 removes the premise: the scrollback is ours to
|
|
300
|
+
* erase, so a card can simply be re-rendered where it stands.
|
|
301
|
+
*
|
|
302
|
+
* A RUNNING card is exempt. Its height is E2/DC-43's — it grows from
|
|
303
|
+
* its own content — and a global "show everything" has no business
|
|
304
|
+
* reaching into a card whose content is still arriving.
|
|
305
|
+
*/
|
|
306
|
+
toggleExpanded(): void;
|
|
307
|
+
/** DC-50 — the switch itself, for the CLI's affordance text. */
|
|
308
|
+
expandedAll(): boolean;
|
|
316
309
|
/** W18: the status row's right-aligned hint is part of the status
|
|
317
310
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
318
311
|
* must survive repaints). */
|
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 { CAP_PREVIEW, thinkingRow,
|
|
55
|
+
import { CAP_PREVIEW, thinkingRow, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, foldCountsObjects, foldTerms, focusToken, foldLine, gutterCut, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, visibleWidth, breathFrame, } 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
|
// R5 — the transcript viewer's PURE projection. The compositor supplies
|
|
@@ -212,6 +212,30 @@ export class Body {
|
|
|
212
212
|
/** DC-34 — the previous frame's width; the reach-back guard is for a
|
|
213
213
|
* WIDTH change, which re-indexes the model, not a height change. */
|
|
214
214
|
#lastW = 0;
|
|
215
|
+
/** R14 — the geometry the last SETTLE ran at. A winch back to the
|
|
216
|
+
* same size is not a resize and must not erase the scrollback. */
|
|
217
|
+
/** R14 — the geometry the last frame PAINTED, and the snapshot of it
|
|
218
|
+
* taken when a winch opens a settle window.
|
|
219
|
+
*
|
|
220
|
+
* Three versions of this question were wrong before this one.
|
|
221
|
+
* Seeding in `enter()` never ran under the unit pool (it bails on a
|
|
222
|
+
* non-TTY), so a same-size winch erased the scrollback. Comparing
|
|
223
|
+
* against the CURRENT frame's geometry let an ordinary frame render
|
|
224
|
+
* at the new size between the winch and the settle and eat the
|
|
225
|
+
* change, so a real resize silently stopped reprinting. Seeding once
|
|
226
|
+
* on the first frame had the same hole whenever the first frame had
|
|
227
|
+
* not run yet.
|
|
228
|
+
*
|
|
229
|
+
* The question the settle actually asks is "is the terminal holding
|
|
230
|
+
* a rendering made at a DIFFERENT geometry than the one we have
|
|
231
|
+
* now" — so the comparison is against what was on screen when the
|
|
232
|
+
* winch arrived, snapshotted then, once per storm. */
|
|
233
|
+
#paintedW = 0;
|
|
234
|
+
#paintedH = 0;
|
|
235
|
+
#winchFromW = 0;
|
|
236
|
+
#winchFromH = 0;
|
|
237
|
+
/** DC-50 — the ONE expansion switch ctrl+o flips. */
|
|
238
|
+
#expandedAll = false;
|
|
215
239
|
// KC1 §6: the composer's recorded extent — the row count the last
|
|
216
240
|
// frame drew (exit's clear walks it) and the row its CHA parked the
|
|
217
241
|
// cursor on (the steady frame's relative anchor). N = 1 reproduces
|
|
@@ -237,18 +261,12 @@ export class Body {
|
|
|
237
261
|
#mdBase = 0;
|
|
238
262
|
#toolCells = new Map(); // callId → cell index (parallel tools)
|
|
239
263
|
// W15: the collapsed (cut) tool cells — committed cells whose last
|
|
240
|
-
// rendered row carried the "ctrl+o" affordance
|
|
241
|
-
//
|
|
264
|
+
// rendered row carried the "ctrl+o" affordance.
|
|
265
|
+
//
|
|
266
|
+
// DC-50 / R14: the expand key no longer WALKS this. It is the
|
|
267
|
+
// VIEWER's index now (ctrl+r, `#viewerEntries`) — the two surfaces
|
|
268
|
+
// coexist and §9 says so. It is kept, not retired, for that reader.
|
|
242
269
|
#collapsed = [];
|
|
243
|
-
/** R4 (C1) — the ring walk is by IDENTITY, not by a modular pointer.
|
|
244
|
-
* `#collapsed` is unshifted on every commit that carries the key, so
|
|
245
|
-
* a numeric pointer's target silently CHANGED whenever a new fold
|
|
246
|
-
* landed mid-cycle: the ring was not stable under itself, and the
|
|
247
|
-
* next press opened something other than what the last press
|
|
248
|
-
* implied. This set records what the current cycle has already
|
|
249
|
-
* opened; the walk takes the newest entry not in it, and empties it
|
|
250
|
-
* when every entry has been seen. */
|
|
251
|
-
#opened = new Set();
|
|
252
270
|
// W14: the turn records — one per userLine, the fold-hold's state
|
|
253
271
|
// machine (ended / hasText / folded) plus the folded-turn line's
|
|
254
272
|
// counts (accumulated at toolStart). The cells carry the record's
|
|
@@ -1167,112 +1185,32 @@ export class Body {
|
|
|
1167
1185
|
cutLine(`${p.dim} ${viewerHint(this.#viewer, entries)}${p.reset}`, W),
|
|
1168
1186
|
];
|
|
1169
1187
|
}
|
|
1170
|
-
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* the
|
|
1188
|
+
/*
|
|
1189
|
+
* RETIRED (DC-50 / R14, 2026-09-05) — `expandNext`, `#expandNextRaw`,
|
|
1190
|
+
* `#lastAppend`, and the `#opened` bookkeeping that went with them.
|
|
1191
|
+
*
|
|
1192
|
+
* The whole apparatus served ONE constraint: ADR-0046 §3 forbade
|
|
1193
|
+
* re-rendering a committed card, so the only way to show one's body
|
|
1194
|
+
* was to APPEND a copy further down the transcript. That needed a ring
|
|
1195
|
+
* to choose which card came next, a set to stop the ring repeating
|
|
1196
|
+
* itself, and DC-35's guard to stop a HELD key printing the same four
|
|
1197
|
+
* rows three times — a defect the owner met in the field, each copy
|
|
1198
|
+
* closing with a footer that named something which did not exist.
|
|
1199
|
+
*
|
|
1200
|
+
* Amendment 1 removes the constraint. The terminal's scrollback is
|
|
1201
|
+
* ours to erase, so a committed card is re-rendered where it stands:
|
|
1202
|
+
* nothing to append, nothing to choose between, no repeat to guard.
|
|
1203
|
+
* `toggleExpanded` is the whole of the feature now.
|
|
1204
|
+
*
|
|
1205
|
+
* `#collapsed` STAYS — the viewer (ctrl+r) reads it as its index of
|
|
1206
|
+
* expandable cells, and §9 keeps the two surfaces side by side.
|
|
1207
|
+
*
|
|
1208
|
+
* `expandedCard` does NOT stay, and an earlier draft of this note said
|
|
1209
|
+
* it did. An expanded card is drawn by the ORDINARY card renderer with
|
|
1210
|
+
* `expanded` set — full body, `ctrl+o collapses` in place of the cut
|
|
1211
|
+
* note — which `r14-global-expand` pins. `expandedCard` was the shape
|
|
1212
|
+
* of the APPENDED block and has no caller once the append is gone.
|
|
1189
1213
|
*/
|
|
1190
|
-
expandNext() {
|
|
1191
|
-
const out = this.#expandNextRaw();
|
|
1192
|
-
if (out.kind !== "appended")
|
|
1193
|
-
return out;
|
|
1194
|
-
const lines = out.lines.join("\n");
|
|
1195
|
-
if (this.#lastAppend !== null && this.#lastAppend.lines === lines && this.#lastAppend.atCells === this.#cells.length) {
|
|
1196
|
-
// NOT the same answer as "nothing is folded". The caller says
|
|
1197
|
-
// which, because a reader who pressed the key deserves to know
|
|
1198
|
-
// whether there is nothing to open or whether they are already
|
|
1199
|
-
// looking at it.
|
|
1200
|
-
return { kind: "none", why: "already-last" };
|
|
1201
|
-
}
|
|
1202
|
-
this.#lastAppend = { lines, atCells: this.#cells.length };
|
|
1203
|
-
return out;
|
|
1204
|
-
}
|
|
1205
|
-
#expandNextRaw() {
|
|
1206
|
-
for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
|
|
1207
|
-
const cell = this.#cells[i];
|
|
1208
|
-
if (cell.kind === "tool" && cell.state !== "pending") {
|
|
1209
|
-
cell.expanded = !cell.expanded;
|
|
1210
|
-
this.#mark();
|
|
1211
|
-
return { kind: "toggled" };
|
|
1212
|
-
}
|
|
1213
|
-
// W20: the LIVE task block toggles in place too — the capped
|
|
1214
|
-
// form flips to the full list (the "done-collapse expands
|
|
1215
|
-
// under ctrl+o" claim). The SETTLED block is already full —
|
|
1216
|
-
// no toggle, and its rows carry no affordance, so it never
|
|
1217
|
-
// joins #collapsed (the committed /last append is moot).
|
|
1218
|
-
if (cell.kind === "checklist" && !cell.done) {
|
|
1219
|
-
cell.expanded = !cell.expanded;
|
|
1220
|
-
this.#mark();
|
|
1221
|
-
return { kind: "toggled" };
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (this.#collapsed.length === 0)
|
|
1225
|
-
return { kind: "none" };
|
|
1226
|
-
// R4 (C1) — the newest entry this cycle has not opened yet. When
|
|
1227
|
-
// every entry has been seen the cycle restarts, so the walk is
|
|
1228
|
-
// still "newest back" — it is simply immune to the ring growing
|
|
1229
|
-
// underneath it.
|
|
1230
|
-
if (this.#collapsed.every((i) => this.#opened.has(i)))
|
|
1231
|
-
this.#opened.clear();
|
|
1232
|
-
const idx = this.#collapsed.find((i) => !this.#opened.has(i)) ?? this.#collapsed[0];
|
|
1233
|
-
this.#opened.add(idx);
|
|
1234
|
-
const cell = this.#cells[idx];
|
|
1235
|
-
// R3b — a folded SEGMENT expands to the work it stands for.
|
|
1236
|
-
//
|
|
1237
|
-
// The fold line collapses a run of thinking and tool cells into
|
|
1238
|
-
// one row; without this the run would be unreachable, which is
|
|
1239
|
-
// hiding a durable record behind a summary. The rows are APPENDED
|
|
1240
|
-
// (ADR-0046 — history is never rewritten), exactly as every other
|
|
1241
|
-
// expand in this method does, and they are the cells' OWN renders,
|
|
1242
|
-
// so the expansion cannot drift from what was folded.
|
|
1243
|
-
// R13 — expandNext's FOLD branch retired with the segment fold.
|
|
1244
|
-
// A fold line collapsed a run of cells into one row, so the key had
|
|
1245
|
-
// to be able to open the run; with every call standing as its own
|
|
1246
|
-
// card there is nothing collapsed for it to open, and `#collapsed`
|
|
1247
|
-
// now holds cards alone.
|
|
1248
|
-
if (cell.kind !== "tool")
|
|
1249
|
-
return { kind: "none" };
|
|
1250
|
-
// R13 — and the ROLLUP branch retired with `rolled`.
|
|
1251
|
-
let input = {};
|
|
1252
|
-
try {
|
|
1253
|
-
input = JSON.parse(cell.inputFull);
|
|
1254
|
-
}
|
|
1255
|
-
catch {
|
|
1256
|
-
// the full JSON is always parseable (it was stringified at
|
|
1257
|
-
// toolStart) — the empty fallback never fires
|
|
1258
|
-
}
|
|
1259
|
-
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
1260
|
-
const p = palette();
|
|
1261
|
-
// 0.24.2 ③ — the expansion is a CARD, and `✦` is not its mark: that
|
|
1262
|
-
// glyph is the turn recap's, and one symbol with two meanings is
|
|
1263
|
-
// §4.1. The head row names the call, which is the tie to it that
|
|
1264
|
-
// the mark's sentence used to be.
|
|
1265
|
-
const verb = displayVerb(cell.name);
|
|
1266
|
-
const elapsed = cell.startedAt !== null && cell.doneAt !== null ? ((cell.doneAt - cell.startedAt) / 1000).toFixed(1) : "?";
|
|
1267
|
-
const n = cell.resultText === "" ? 0 : cell.resultText.split("\n").length;
|
|
1268
|
-
return {
|
|
1269
|
-
kind: "appended",
|
|
1270
|
-
lines: expandedCard(verb, toolTarget(cell.name, input), `expanded · ${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`,
|
|
1271
|
-
// TUI2-R2pre ④: the SECTION HEADERS say the act; the payloads
|
|
1272
|
-
// below them (inputFull, resultText) are RAW and byte-identical.
|
|
1273
|
-
[`--- ${verb} input ---`, cell.inputFull, `--- ${verb} output${cell.isError ? " (error)" : ""} ---`, cell.resultText], [cell.isError ? "failed" : "exit 0", `${n} line${n === 1 ? "" : "s"}`, `${elapsed}s`].filter((x) => x !== "").join(" · "), this.#opts.width()),
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
1214
|
// ---- the Dock façade (the CLI's chrome API — same shape as the old dock) ----
|
|
1277
1215
|
/** Docked = the chrome is live (a color TTY with a real size). */
|
|
1278
1216
|
get active() {
|
|
@@ -1506,6 +1444,14 @@ export class Body {
|
|
|
1506
1444
|
onResize() {
|
|
1507
1445
|
if (!this.#isActive())
|
|
1508
1446
|
return;
|
|
1447
|
+
// R14 — the FIRST winch of a storm snapshots what the terminal is
|
|
1448
|
+
// currently holding. A drag fires many winches; the settle must
|
|
1449
|
+
// compare its end against the storm's beginning, not against the
|
|
1450
|
+
// previous winch.
|
|
1451
|
+
if (!this.#resizePending) {
|
|
1452
|
+
this.#winchFromW = this.#paintedW;
|
|
1453
|
+
this.#winchFromH = this.#paintedH;
|
|
1454
|
+
}
|
|
1509
1455
|
this.#resizePending = true;
|
|
1510
1456
|
if (this.#resizeTimer !== null)
|
|
1511
1457
|
clearTimeout(this.#resizeTimer);
|
|
@@ -1533,20 +1479,128 @@ export class Body {
|
|
|
1533
1479
|
this.#screen = [];
|
|
1534
1480
|
this.onResize();
|
|
1535
1481
|
}
|
|
1536
|
-
/**
|
|
1482
|
+
/**
|
|
1483
|
+
* R14 / route B — THE ONE REPAINT A DRAG EARNS IS A REPRINT.
|
|
1484
|
+
*
|
|
1485
|
+
* It used to be `ESC[from;1H ESC[0J` — erase from the recorded live
|
|
1486
|
+
* top down, then redraw the live area, leaving the committed rows
|
|
1487
|
+
* above to whatever the terminal's own reflow had made of them. That
|
|
1488
|
+
* is the shape R10 measured on the owner's real terminal, and it
|
|
1489
|
+
* lost: a grow dropped 16 rows of history (DC-39), a narrow
|
|
1490
|
+
* duplicated four tokens, and the scrolled-off transcript never
|
|
1491
|
+
* reflowed at all. All three are one fault — kiso doing window
|
|
1492
|
+
* arithmetic over rows the terminal had already reflowed underneath
|
|
1493
|
+
* it, with `#scrolledOff` carried across a fold change that makes
|
|
1494
|
+
* every index mean something else.
|
|
1495
|
+
*
|
|
1496
|
+
* So it stops arguing. `2J H 3J` — erase screen, home, erase
|
|
1497
|
+
* scrollback, in THAT order because on Apple Terminal a bare `2J`
|
|
1498
|
+
* scrolls the screen into history and only the following `3J` makes
|
|
1499
|
+
* the state clean — and then the whole committed transcript is
|
|
1500
|
+
* reprinted at the new geometry through the path a fresh terminal
|
|
1501
|
+
* already uses. The terminal ends holding exactly one rendering.
|
|
1502
|
+
*
|
|
1503
|
+
* The declared cost (ADR-0046 Amendment 1): everything the terminal
|
|
1504
|
+
* held before kiso started is erased at the first resize. Measured
|
|
1505
|
+
* on the reference implementation as 0/60 in every direction — this
|
|
1506
|
+
* is the behaviour being adopted, not a regression. kiso's own
|
|
1507
|
+
* record is untouched: the session log holds it, `--resume` replays
|
|
1508
|
+
* it, and G4' asserts the 0/60 so the cost cannot drift silently.
|
|
1509
|
+
*
|
|
1510
|
+
* D-B2: EVERY settled resize reprints, height-only included. A
|
|
1511
|
+
* SIGWINCH at the same geometry emits nothing at all.
|
|
1512
|
+
*/
|
|
1537
1513
|
#settleResize() {
|
|
1538
1514
|
if (!this.#resizePending || !this.#isActive())
|
|
1539
1515
|
return;
|
|
1540
1516
|
this.#resizePending = false;
|
|
1541
1517
|
const H = this.#opts.height();
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1518
|
+
const W = this.#opts.width();
|
|
1519
|
+
// D-B2's other half: a winch that did not change the geometry is
|
|
1520
|
+
// not a resize. Emitting the erase for it would throw away the
|
|
1521
|
+
// terminal's scrollback for nothing (the V6-1 idempotence case).
|
|
1522
|
+
if (this.#winchFromW === W && this.#winchFromH === H)
|
|
1523
|
+
return;
|
|
1524
|
+
// §9.2 — the viewer is closed first. Nothing commits while it is
|
|
1525
|
+
// up and a reprint is a commit storm; the user reopens it. One
|
|
1526
|
+
// keypress, stated rather than hidden.
|
|
1527
|
+
if (this.#viewer !== null) {
|
|
1528
|
+
this.#viewer = null;
|
|
1529
|
+
this.#viewerWasUp = true;
|
|
1530
|
+
}
|
|
1531
|
+
this.#reprint();
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* R14 — ERASE THE TERMINAL AND PRINT THE SESSION AGAIN.
|
|
1535
|
+
*
|
|
1536
|
+
* Two callers: a settled resize, and DC-50's ctrl+o. They are the
|
|
1537
|
+
* same act — the rendering the terminal holds is wrong (wrong
|
|
1538
|
+
* geometry, or wrong expansion state) and the model is the only
|
|
1539
|
+
* authority on what it should be — so they share the path rather
|
|
1540
|
+
* than growing two.
|
|
1541
|
+
*/
|
|
1542
|
+
#reprint() {
|
|
1543
|
+
const H = this.#opts.height();
|
|
1544
|
+
this.#write("\x1b[2J\x1b[H\x1b[3J");
|
|
1545
|
+
// The terminal now holds nothing, so every record of what it held
|
|
1546
|
+
// is void. `#scrolledOff` is the frontier of what reached its
|
|
1547
|
+
// scrollback: after the erase, that is zero — which is also what
|
|
1548
|
+
// releases the committed cells to be refolded at the new width
|
|
1549
|
+
// without contradicting anything, the thing DC-34's frontier rule
|
|
1550
|
+
// existed to prevent.
|
|
1551
|
+
this.#scrolledOff = 0;
|
|
1552
|
+
for (let i = 0; i < this.#committed; i += 1)
|
|
1553
|
+
this.#lineCache[i] = null;
|
|
1554
|
+
this.#screen = new Array(Math.max(1, H)).fill(NOT_PAINTED);
|
|
1555
|
+
this.#cursorRow = 1;
|
|
1545
1556
|
this.#fullRedraw = true;
|
|
1546
1557
|
this.#resizeFrame = true;
|
|
1547
1558
|
this.#dirty = true;
|
|
1548
1559
|
this.render();
|
|
1549
1560
|
}
|
|
1561
|
+
/**
|
|
1562
|
+
* DC-50 — ctrl+o flips ONE switch, and every settled card obeys it.
|
|
1563
|
+
*
|
|
1564
|
+
* The walk this replaces existed because ADR-0046 §3 forbade
|
|
1565
|
+
* re-rendering a committed card, so the only way to show one's body
|
|
1566
|
+
* was to APPEND a copy further down — with a ring to decide which
|
|
1567
|
+
* card was next, `#opened` to keep the ring from repeating, and
|
|
1568
|
+
* `#lastAppend` to stop a held key printing the same rows three times
|
|
1569
|
+
* (DC-35). Amendment 1 removes the premise: the scrollback is ours to
|
|
1570
|
+
* erase, so a card can simply be re-rendered where it stands.
|
|
1571
|
+
*
|
|
1572
|
+
* A RUNNING card is exempt. Its height is E2/DC-43's — it grows from
|
|
1573
|
+
* its own content — and a global "show everything" has no business
|
|
1574
|
+
* reaching into a card whose content is still arriving.
|
|
1575
|
+
*/
|
|
1576
|
+
toggleExpanded() {
|
|
1577
|
+
this.#expandedAll = !this.#expandedAll;
|
|
1578
|
+
for (const cell of this.#cells) {
|
|
1579
|
+
// SETTLED CONTENT, not "done". A card parked for approval is
|
|
1580
|
+
// `state: "approval"`, `done: false` — and its content is not
|
|
1581
|
+
// still arriving: the diff is complete and the card is waiting
|
|
1582
|
+
// for a human to read it and decide. The mechanism this
|
|
1583
|
+
// replaces toggled it (any state but "pending"), and the
|
|
1584
|
+
// dispatch comment said why: "the approval pause is exactly
|
|
1585
|
+
// when the user reads a cut diff, and the key must answer then,
|
|
1586
|
+
// never after the run." A `done`-only switch answers with
|
|
1587
|
+
// nothing at the one moment the answer matters most.
|
|
1588
|
+
//
|
|
1589
|
+
// The exemption is for the card still GROWING — E2 and DC-43
|
|
1590
|
+
// own its height, and a global "show everything" has no
|
|
1591
|
+
// business reaching into content that is still arriving. That
|
|
1592
|
+
// is the ruling's reason; `done` alone was its wording.
|
|
1593
|
+
if (cell.kind !== "tool")
|
|
1594
|
+
continue;
|
|
1595
|
+
if (cell.done || cell.state === "approval")
|
|
1596
|
+
cell.expanded = this.#expandedAll;
|
|
1597
|
+
}
|
|
1598
|
+
this.#reprint();
|
|
1599
|
+
}
|
|
1600
|
+
/** DC-50 — the switch itself, for the CLI's affordance text. */
|
|
1601
|
+
expandedAll() {
|
|
1602
|
+
return this.#expandedAll;
|
|
1603
|
+
}
|
|
1550
1604
|
/** W18: the status row's right-aligned hint is part of the status
|
|
1551
1605
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
1552
1606
|
* must survive repaints). */
|
|
@@ -1949,6 +2003,10 @@ export class Body {
|
|
|
1949
2003
|
if (H < 4)
|
|
1950
2004
|
return;
|
|
1951
2005
|
this.#lastH = H;
|
|
2006
|
+
// R14 — the geometry THIS frame painted. `onResize` snapshots it
|
|
2007
|
+
// at the moment the winch arrives; see `#winchFromW`.
|
|
2008
|
+
this.#paintedW = W;
|
|
2009
|
+
this.#paintedH = H;
|
|
1952
2010
|
const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: H };
|
|
1953
2011
|
// V6-1 (the screen-state == frame-state rule): the resize's first
|
|
1954
2012
|
// frame — the terminal's reflow re-wrapped the committed content at
|
|
@@ -2384,11 +2442,9 @@ export class Body {
|
|
|
2384
2442
|
const hidesRows = cell.kind === "tool" && lines.some((l) => l.includes("ctrl+o"));
|
|
2385
2443
|
if (isFoldHead || hidesRows) {
|
|
2386
2444
|
this.#collapsed.unshift(i);
|
|
2387
|
-
// R4a
|
|
2388
|
-
//
|
|
2389
|
-
//
|
|
2390
|
-
// "the last one", and repeats walk back from there.
|
|
2391
|
-
this.#opened.clear();
|
|
2445
|
+
// R4a's walk reset retired with the walk (DC-50): there is no
|
|
2446
|
+
// "which one does it open" any more — one press opens all of
|
|
2447
|
+
// them.
|
|
2392
2448
|
}
|
|
2393
2449
|
this.#lineCache[i] = lines;
|
|
2394
2450
|
const placed = this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines);
|
|
@@ -2944,38 +3000,31 @@ export class Body {
|
|
|
2944
3000
|
// sit with the live band empty and the chrome at its minimum — the
|
|
2945
3001
|
// one-way part of a movement that otherwise goes both ways.
|
|
2946
3002
|
if (this.#resizeFrame) {
|
|
2947
|
-
//
|
|
2948
|
-
// window is the terminal's own scroll — it reflows the old
|
|
2949
|
-
// content and pushes the overflow into its scrollback before we
|
|
2950
|
-
// are called — so emitting our own LFs on top put the same rows
|
|
2951
|
-
// in twice (TT-1B: twelve rows of a forty-line burst). The
|
|
2952
|
-
// counter adopts the terminal's work, and the held screen is
|
|
2953
|
-
// discarded because a reflow invalidates every row of it: the
|
|
2954
|
-
// next diff repaints the whole screen, which is exactly what a
|
|
2955
|
-
// resize needs.
|
|
2956
|
-
// DC-34 — NO HIGH-WATER MARK ON A RESIZE.
|
|
3003
|
+
// R14 / route B — THE RESIZE FRAME SCROLLS LIKE ANY OTHER.
|
|
2957
3004
|
//
|
|
2958
|
-
//
|
|
2959
|
-
//
|
|
2960
|
-
//
|
|
2961
|
-
//
|
|
2962
|
-
//
|
|
3005
|
+
// Everything this branch used to hold is retired with the
|
|
3006
|
+
// window arithmetic it served. It adopted the terminal's own
|
|
3007
|
+
// reflow-scroll into `#scrolledOff` (REL-0152-R1), then had to
|
|
3008
|
+
// stop adopting it on a widen and keep adopting it on a narrow
|
|
3009
|
+
// (DC-34), because the counter it was feeding meant a different
|
|
3010
|
+
// thing at every fold width. There is no such counter to feed
|
|
3011
|
+
// now: `#settleResize` erased the terminal and set
|
|
3012
|
+
// `#scrolledOff` to zero, so the frontier is real and empty and
|
|
3013
|
+
// the frame simply reprints from the top.
|
|
2963
3014
|
//
|
|
2964
|
-
//
|
|
2965
|
-
//
|
|
2966
|
-
//
|
|
2967
|
-
//
|
|
2968
|
-
//
|
|
2969
|
-
//
|
|
2970
|
-
//
|
|
2971
|
-
//
|
|
2972
|
-
if (this.#refolded)
|
|
2973
|
-
this.#scrolledOff = Math.max(this.#scrolledOff, Math.max(0, Math.min(skip, all.length)));
|
|
3015
|
+
// Falling through to `#emitScroll` is not a convenience — it is
|
|
3016
|
+
// the whole point. The transcript above the last screenful has
|
|
3017
|
+
// to reach the scrollback, chunked and staged from the model,
|
|
3018
|
+
// exactly as a resumed session's replay does. Without it the
|
|
3019
|
+
// reprint painted the last screenful onto a terminal whose
|
|
3020
|
+
// history it had just erased, and everything above was simply
|
|
3021
|
+
// GONE: `dc34-widen-seam` measured 36 tokens missing from the
|
|
3022
|
+
// scrollback the first time this branch was left in place.
|
|
2974
3023
|
this.#refolded = false;
|
|
2975
3024
|
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
2976
3025
|
this.#resizeFrame = false;
|
|
2977
3026
|
}
|
|
2978
|
-
|
|
3027
|
+
if (!overlay) {
|
|
2979
3028
|
// DC-46 — THE ROWS THAT LEAVE THE WINDOW SCROLL, and the target
|
|
2980
3029
|
// is `skip` itself.
|
|
2981
3030
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.25.0"
|
|
39
39
|
}
|
|
40
40
|
}
|