@vincemakes/kiso-tui 0.16.8 → 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.
@@ -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
  } | {
@@ -52,7 +52,7 @@ import { MOUSE_OFF } from "./editor.js";
52
52
  import { atPanelRows, bandHeader } from "./at-picker.js";
53
53
  // TUI2-R2 ②: the session picker's rows — the band's third occupant.
54
54
  import { sessionPickerRows } from "./session-picker.js";
55
- import { Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, isExploreTool, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
55
+ import { 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
@@ -69,6 +69,11 @@ const NOT_PAINTED = "\u0000never";
69
69
  * that a single resize still feels immediate. */
70
70
  const RESIZE_SETTLE_MS = 80;
71
71
  const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
72
+ /** R3i — how many calls in flight the act window shows at once. Beyond
73
+ * it the block would grow with the model's parallelism, which is the
74
+ * same unbounded height the projection exists to remove; the rest are
75
+ * COUNTED, never dropped silently. */
76
+ const LIVE_ACT_HEADS = 3;
72
77
  /** W13 / TUI2-R1 (B) — a rolled run's TITLE: the exploration sentence on
73
78
  * a mixed run, W13's verb+count on a single-name one. */
74
79
  function rolledTitle(cell) {
@@ -174,7 +179,7 @@ function openSegment(turn, now) {
174
179
  const last = turn.segments[turn.segments.length - 1];
175
180
  if (last !== undefined && last.closedAt === null)
176
181
  return last;
177
- const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), folded: false, spilled: false, headCell: null, cells: [] };
182
+ const fresh = { openedAt: now, closedAt: null, reads: 0, edits: 0, others: new Map(), seen: new Map(), thinkingMs: 0, thinkingSince: null, folded: false, spilled: false, headCell: null, foldKey: null, cells: [] };
178
183
  turn.segments.push(fresh);
179
184
  return fresh;
180
185
  }
@@ -183,8 +188,21 @@ function openSegment(turn, now) {
183
188
  * keeps a zero-cell segment from ever existing. */
184
189
  function closeSegment(turn, now) {
185
190
  const last = turn?.segments[turn.segments.length - 1];
186
- if (last !== undefined && last.closedAt === null)
187
- last.closedAt = now;
191
+ if (last === undefined || last.closedAt !== null)
192
+ return;
193
+ stopThinking(last, now);
194
+ last.closedAt = now;
195
+ }
196
+ /** R3i — the segment's thinking clock stops. It runs from the first
197
+ * thinking delta of a stretch and stops at the first NON-thinking
198
+ * event, the same rule the CLI applies to the turn — so `thought Ns`
199
+ * is thinking time at every scale and never a wall clock wearing the
200
+ * word (the R3g defect, kept closed at the new scale). */
201
+ function stopThinking(seg, now) {
202
+ if (seg === undefined || seg.thinkingSince === null)
203
+ return;
204
+ seg.thinkingMs += Math.max(0, now - seg.thinkingSince);
205
+ seg.thinkingSince = null;
188
206
  }
189
207
  /** W20 — the whole-table-replace comparison: the live task block only
190
208
  * redraws when the items actually changed (the task extension's
@@ -296,7 +314,17 @@ export class Body {
296
314
  // rendered row carried the "ctrl+r" affordance; the expand key's
297
315
  // cycling pointer walks this list from the newest back.
298
316
  #collapsed = [];
299
- #expandPtr = 0;
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;
300
328
  // W14: the turn records — one per userLine, the fold-hold's state
301
329
  // machine (ended / hasText / folded) plus the folded-turn line's
302
330
  // counts (accumulated at toolStart). The cells carry the record's
@@ -466,7 +494,20 @@ export class Body {
466
494
  // the cell belongs to, and a stamp taken first records the
467
495
  // PREVIOUS segment (or none at all) — which left the thinking
468
496
  // row standing outside the fold it should have led.
469
- openSegment(this.#turns[this.#turns.length - 1], Date.now());
497
+ const seg = openSegment(this.#turns[this.#turns.length - 1], Date.now());
498
+ // R3i: the stretch's thinking clock starts HERE — at the first
499
+ // delta of this stretch, the same moment the CLI starts the
500
+ // turn's — and stops at the next non-thinking event below.
501
+ if (seg !== null && seg.thinkingSince === null)
502
+ seg.thinkingSince = Date.now();
503
+ // R3i: and the beat starts HERE. Law 1.4 says "a running thought
504
+ // twinkles", and `#armSpinner`'s own predicate has always
505
+ // included an open thinking cell — but the only caller was
506
+ // `toolRunning`, so a stretch that thought and did nothing else
507
+ // never moved at all. The line's seconds are a frame-time
508
+ // derivation, so without the beat they also never ticked: the
509
+ // row read `thinking 0s` for as long as the model thought.
510
+ this.#armSpinner();
470
511
  this.#stampSegment();
471
512
  }
472
513
  this.#mark();
@@ -474,6 +515,10 @@ export class Body {
474
515
  thinkingEnd() {
475
516
  const last = this.#cells[this.#cells.length - 1];
476
517
  if (last !== undefined && last.kind === "thinking" && !last.done) {
518
+ // R3i: every closer — text, a notice, a terminal label, the next
519
+ // turn — routes through here, so the clock cannot keep running
520
+ // past the thing that ended it.
521
+ stopThinking(this.#turns[this.#turns.length - 1]?.segments.at(-1), Date.now());
477
522
  last.done = true;
478
523
  this.#lastThinking = last.text;
479
524
  if (!this.#isActive())
@@ -527,6 +572,18 @@ export class Body {
527
572
  // of a target the term has already counted; an ACT-counting
528
573
  // tool (a search, a shell command) always bumps, because two
529
574
  // searches for the same pattern really are two searches.
575
+ // R3i phase 5 — an ANSWER is words, and words do not fold (law
576
+ // 1.7). `ask_user` closes the open stretch exactly as prose
577
+ // does, and never joins one: absorbed into `1 × ask_user`,
578
+ // what the human said would be gone from the screen — and the
579
+ // one thing a summary must not do is speak for the human.
580
+ if (name === "ask_user") {
581
+ // no stamp: it belongs to NO stretch, so no fold can speak
582
+ // for it — the same standing a block of prose has.
583
+ closeSegment(turn, Date.now());
584
+ this.#mark();
585
+ return;
586
+ }
530
587
  const target = foldCountsObjects(name) ? toolTarget(name, input) : null;
531
588
  const bump = (rec) => {
532
589
  if (target === null)
@@ -554,6 +611,9 @@ export class Body {
554
611
  // file read once per segment is one file in each segment's
555
612
  // terms and one file in the turn's.
556
613
  const seg = openSegment(turn, Date.now());
614
+ // R3i: a tool call is a NON-thinking event — the clock stops,
615
+ // exactly as the CLI's does at the same boundary.
616
+ stopThinking(seg ?? undefined, Date.now());
557
617
  if (seg !== null && bump(seg)) {
558
618
  if (name === "read_file")
559
619
  seg.reads += 1;
@@ -974,6 +1034,73 @@ export class Body {
974
1034
  }
975
1035
  return -1;
976
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
+ }
977
1104
  expandNext() {
978
1105
  for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
979
1106
  const cell = this.#cells[i];
@@ -995,8 +1122,14 @@ export class Body {
995
1122
  }
996
1123
  if (this.#collapsed.length === 0)
997
1124
  return { kind: "none" };
998
- const idx = this.#collapsed[this.#expandPtr % this.#collapsed.length];
999
- this.#expandPtr += 1;
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);
1000
1133
  const cell = this.#cells[idx];
1001
1134
  // R3b — a folded SEGMENT expands to the work it stands for.
1002
1135
  //
@@ -1075,7 +1208,20 @@ export class Body {
1075
1208
  // boundaries survive where they carry meaning (the write that
1076
1209
  // splits two explore runs); they simply no longer bound what
1077
1210
  // the key can reach.
1078
- for (const j of foldTurn.segments.flatMap((sg) => sg.cells).sort((a, b) => a - b)) {
1211
+ // DECLARED SUPERSESSION (R3i phase 3) the expansion covers THIS
1212
+ // STRETCH, and only this stretch.
1213
+ //
1214
+ // R3f widened it to the whole turn, and had to: R3d had made
1215
+ // the fold the TURN's while the expansion still walked one
1216
+ // segment, so a line claiming `read 3 files · edited 1 file`
1217
+ // opened only the reads — work named and then withheld, the one
1218
+ // thing this file's first gate forbids. R3i moves the fold back
1219
+ // to the stretch, so the pairing is exact again: every stretch
1220
+ // has its OWN line and its own key, and each key opens the work
1221
+ // its line named. Keeping the turn walk would break the same
1222
+ // rule from the other side — two lines, each opening
1223
+ // everything, each header describing rows the other also shows.
1224
+ for (const j of seg.cells) {
1079
1225
  if (j < idx)
1080
1226
  continue;
1081
1227
  const c = this.#cells[j];
@@ -1098,8 +1244,29 @@ export class Body {
1098
1244
  // the header names what the FOLD said — the turn's terms — so the
1099
1245
  // line you pressed and the block it opens agree. It used to name
1100
1246
  // 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] };
1247
+ const t = this.#stretchTerms(seg);
1248
+ const head = foldTerms(t.calls.find(([n]) => n === "read_file")?.[1] ?? 0, t.calls.find(([n]) => n === "edit_file")?.[1] ?? 0, t.calls.filter(([n]) => n !== "read_file" && n !== "edit_file"));
1249
+ // R3i phase 4 — THE FOOTER TELLS THE TRUTH ABOUT THIS PATH.
1250
+ //
1251
+ // The rows come from the rollup's own projection, whose last
1252
+ // row reads `└ ctrl+r collapses` — true where it was written
1253
+ // (the LIVE toggle, which really does close again) and false
1254
+ // here. A committed row is ink: ADR-0046 forbids rewriting
1255
+ // history, so nothing about this block can be taken back. The
1256
+ // next press opens the NEXT fold, and the row now says so.
1257
+ const closing = rows.length > 0 && /ctrl\+r collapses/.test(rows[rows.length - 1] ?? "");
1258
+ const body = closing ? rows.slice(0, -1) : rows;
1259
+ return {
1260
+ kind: "appended",
1261
+ lines: [
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}`,
1266
+ ...body,
1267
+ ` ${p.dim}└ end of expansion · ${this.#nextFoldHint()}${p.reset}`,
1268
+ ],
1269
+ };
1103
1270
  }
1104
1271
  if (cell.kind !== "tool")
1105
1272
  return { kind: "none" };
@@ -1574,6 +1741,239 @@ export class Body {
1574
1741
  this.render();
1575
1742
  }
1576
1743
  // ---- the one writer ----
1744
+ /**
1745
+ * R3i phase 2 — THE LIVE PROJECTION.
1746
+ *
1747
+ * One definition, called from the natural path and from inside the
1748
+ * force-commit loop, because two copies of "what the live region
1749
+ * looks like" is two answers to one question.
1750
+ *
1751
+ * The change this phase makes, and the ONLY one: the cells of the
1752
+ * OPEN stretch no longer each hold a row. The stretch is one line —
1753
+ * the same line the settle will keep, in the present tense — plus
1754
+ * the calls actually in flight. A completed call renders nothing;
1755
+ * its count rides the line.
1756
+ *
1757
+ * What it fixes: a 28-call turn used to spend 28 rows of a 30-row
1758
+ * live region, so overflow was the NORM on real turns rather than
1759
+ * the edge — and a turn that overflows may not fold (R3f: a line
1760
+ * cannot claim rows already in the scrollback), which is why the
1761
+ * fold missed exactly the turns it exists for. The block's height
1762
+ * no longer depends on the call count at all.
1763
+ *
1764
+ * What it does NOT change: nothing about what commits or when. The
1765
+ * hold is untouched, the force-commit cap is untouched, and the
1766
+ * settle still produces the same fold it did before. That is the
1767
+ * charter's line between this phase and the next.
1768
+ */
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) {
1784
+ const out = [];
1785
+ const focus = this.#focusIndex();
1786
+ const turn = this.#turns[this.#turns.length - 1];
1787
+ const open = turn !== undefined && !turn.ended ? (turn.segments[turn.segments.length - 1] ?? null) : null;
1788
+ const openSeg = open !== null && open.closedAt === null ? open : null;
1789
+ let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
1790
+ let stretchDrawn = false;
1791
+ for (let i = this.#committed; i < this.#cells.length; i += 1) {
1792
+ const cell = this.#cells[i];
1793
+ const inOpen = openSeg !== null && openSeg.cells.includes(i);
1794
+ if (inOpen) {
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)
1805
+ continue;
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;
1814
+ }
1815
+ const rows = cellComponent(cell).render(W, ctx);
1816
+ // the head row carries the affordance; the tint lands on it and
1817
+ // nowhere else, which is what makes "exactly one" structural
1818
+ if (i === focus && rows.length > 0)
1819
+ rows[0] = focusToken(rows[0], W);
1820
+ out.push(...this.#space(i, prev, rows));
1821
+ prev = rows;
1822
+ }
1823
+ return out;
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
+ }
1937
+ /** R3i — the open stretch's phase. It is THINKING while a thinking
1938
+ * cell of it is still open and no call has started; otherwise it is
1939
+ * ACTING. The tense follows the phase, and the phase is what the
1940
+ * human is watching happen. */
1941
+ #stretchPhase(seg) {
1942
+ return seg.thinkingSince !== null && seg.reads === 0 && seg.edits === 0 && seg.others.size === 0 ? "thinking" : "acting";
1943
+ }
1944
+ /** R3i — the open stretch's terms, in the shape the line renders. */
1945
+ #stretchTerms(seg) {
1946
+ const ms = seg.thinkingMs + (seg.thinkingSince === null ? 0 : Math.max(0, Date.now() - seg.thinkingSince));
1947
+ // R3i: a call in TROUBLE does not contribute to the work terms.
1948
+ // The counts are taken at toolStart, before the outcome is known,
1949
+ // so a denied write would otherwise fold as `wrote 1 file` beside
1950
+ // the clause admitting it was refused — the line saying, in one
1951
+ // breath, that the file was written and that it was not. The
1952
+ // trouble clause is where those calls are counted.
1953
+ const bad = new Map();
1954
+ for (const j of seg.cells) {
1955
+ const c = this.#cells[j];
1956
+ if (c === undefined || c.kind !== "tool" || !this.#cellInTrouble(j))
1957
+ continue;
1958
+ bad.set(c.name, (bad.get(c.name) ?? 0) + 1);
1959
+ }
1960
+ const net = (name, n) => Math.max(0, n - (bad.get(name) ?? 0));
1961
+ const calls = [];
1962
+ if (net("read_file", seg.reads) > 0)
1963
+ calls.push(["read_file", net("read_file", seg.reads)]);
1964
+ if (net("edit_file", seg.edits) > 0)
1965
+ calls.push(["edit_file", net("edit_file", seg.edits)]);
1966
+ for (const [name, n] of seg.others)
1967
+ if (net(name, n) > 0)
1968
+ calls.push([name, net(name, n)]);
1969
+ const targets = [];
1970
+ for (const j of seg.cells) {
1971
+ const c = this.#cells[j];
1972
+ if (c !== undefined && c.kind === "tool")
1973
+ targets.push(toolTarget(c.name, JSON.parse(c.inputFull)));
1974
+ }
1975
+ return { thoughtSeconds: Math.round(ms / 1000), calls, targets, trouble: this.#segmentTroubleTerms(seg) };
1976
+ }
1577
1977
  /** The live region's scalar — the unit tests assert the cap directly
1578
1978
  * (the e2e gate pins the screen consequence). W11: the formula's
1579
1979
  * blanks are join artifacts — the count includes them (they are real
@@ -1603,16 +2003,25 @@ export class Body {
1603
2003
  inputExtra +
1604
2004
  queueRows.length);
1605
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.
1606
2019
  const ctx = { spinnerI: this.#spinnerI, now: Date.now(), height: this.#opts.height() };
1607
2020
  const W = this.#opts.width();
1608
- let lines = 0;
1609
- let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
1610
- for (let i = this.#committed; i < this.#cells.length; i += 1) {
1611
- const rows = cellComponent(this.#cells[i]).render(W, ctx);
1612
- lines += this.#space(i, prev, rows).length;
1613
- prev = rows;
1614
- }
1615
- 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;
1616
2025
  }
1617
2026
  /** The lines committed THIS frame — the writes land in the frame's
1618
2027
  * committed section (the rows just above the live region). */
@@ -1721,18 +2130,7 @@ export class Body {
1721
2130
  // by construction), so the marker can never point at a cell the
1722
2131
  // key would not take — which is the only way a focus marker is
1723
2132
  // worth having.
1724
- const focus = this.#focusIndex();
1725
- let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
1726
- for (let i = this.#committed; i < this.#cells.length; i += 1) {
1727
- const cell = this.#cells[i];
1728
- const rows = cellComponent(cell).render(W, ctx);
1729
- // the head row carries the affordance; the tint lands on it and
1730
- // nowhere else, which is what makes "exactly one" structural
1731
- if (i === focus && rows.length > 0)
1732
- rows[0] = focusToken(rows[0], W);
1733
- liveLines.push(...this.#space(i, prev, rows));
1734
- prev = rows;
1735
- }
2133
+ liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
1736
2134
  }
1737
2135
  // 3. the FORCE commits — the live region's hard cap H−1: overflow
1738
2136
  // commits the oldest live cell UNCONDITIONALLY (the one sharp
@@ -1751,21 +2149,9 @@ export class Body {
1751
2149
  // visible above it.
1752
2150
  this.#markSpilled(this.#committed);
1753
2151
  this.#commitCell(this.#committed, W, ctx);
1754
- liveLines = [];
1755
- {
1756
- // TUI2-R2 ⑤: the focus re-derives after a commit the cell it
1757
- // pointed at may have just left the live region
1758
- const focus = this.#focusIndex();
1759
- let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
1760
- for (let i = this.#committed; i < this.#cells.length; i += 1) {
1761
- const cell = this.#cells[i];
1762
- const rows = cellComponent(cell).render(W, ctx);
1763
- if (i === focus && rows.length > 0)
1764
- rows[0] = focusToken(rows[0], W);
1765
- liveLines.push(...this.#space(i, prev, rows));
1766
- prev = rows;
1767
- }
1768
- }
2152
+ // TUI2-R2 ⑤: the focus re-derives after a commit — the cell it
2153
+ // pointed at may have just left the live region.
2154
+ liveLines = this.#liveProjection(W, ctx, H - 4 - inputExtra - queueRows.length);
1769
2155
  }
1770
2156
  // 4. the geometry — the live region's first row:
1771
2157
  // liveTop = min(totalCommitted, H - liveRows) + 1 — the screen
@@ -1905,7 +2291,29 @@ export class Body {
1905
2291
  #space(i, prev, rows) {
1906
2292
  if (i > 0 && this.#cells[i]?.kind === "md" && this.#cells[i - 1]?.kind === "md")
1907
2293
  return rows;
1908
- return bodySpacing(prev, rows);
2294
+ return bodySpacing(this.#lastDrawn(i, prev), rows);
2295
+ }
2296
+ /**
2297
+ * R3i — the previous DRAWN sibling, not the previous cell.
2298
+ *
2299
+ * The spacing formula reads what stood above; a cell that rendered
2300
+ * nothing did not stand above anything. Since R3d whole families of
2301
+ * cells render `[]` — the members a fold speaks for — and the
2302
+ * formula was reading that empty array as "a zero-row sibling", so a
2303
+ * multi-row block following a fold lost the blank that belongs above
2304
+ * it. The defect predates this round (any folded turn followed by a
2305
+ * raw block has it); R3i's projection is what finally put a test on
2306
+ * the path.
2307
+ */
2308
+ #lastDrawn(i, prev) {
2309
+ if (prev !== null && prev.length > 0)
2310
+ return prev;
2311
+ for (let j = i - 1; j >= 0; j -= 1) {
2312
+ const cached = this.#lineCache[j];
2313
+ if (cached !== null && cached !== undefined && cached.length > 0)
2314
+ return cached;
2315
+ }
2316
+ return prev;
1909
2317
  }
1910
2318
  /** Commit the cell at index i: render + cache its lines (immutable —
1911
2319
  * the force-committed form freezes at the current render), advance
@@ -1962,6 +2370,67 @@ export class Body {
1962
2370
  * denial keeps all twenty rows. The alternative is a screen that says
1963
2371
  * `✦ thought 3s · 20 reads` while a write was refused inside it.
1964
2372
  */
2373
+ /**
2374
+ * R3i — the trouble the stretch met, as the line's own terms.
2375
+ *
2376
+ * Law 1.3: an outcome is stated in WORDS, "the only form that
2377
+ * survives a pipe". So the kind is a different word, never a
2378
+ * different colour — `2 failed`, `1 denied`, `1 interrupted` — and
2379
+ * the failure's identity rides with it. In this phase the terms are
2380
+ * only DRAWN (the live line names trouble the moment it happens);
2381
+ * whether trouble still blocks the fold is the next phase's ruling.
2382
+ */
2383
+ /** R3i — is this cell one the fold must not count as work done? */
2384
+ #cellInTrouble(i) {
2385
+ const c = this.#cells[i];
2386
+ if (c === undefined || c.kind !== "tool")
2387
+ return false;
2388
+ return c.isError || c.reason !== null || c.verdict?.decision === "denied";
2389
+ }
2390
+ #segmentTroubleTerms(seg) {
2391
+ let failed = 0;
2392
+ let denied = 0;
2393
+ let interrupted = 0;
2394
+ let what = "";
2395
+ for (const j of seg.cells) {
2396
+ const c = this.#cells[j];
2397
+ if (c === undefined || c.kind !== "tool")
2398
+ continue;
2399
+ // WHICH call, and WHY. The target alone answers the first and
2400
+ // not the second, and for a policy denial the second is the
2401
+ // whole point: `sub/out.txt` does not tell a human that plan
2402
+ // mode is read-only, and that sentence is the one they act
2403
+ // on. Law 1.3's own words — an outcome is stated in words —
2404
+ // and the ladder cuts this clause last, so it degrades to the
2405
+ // target before it disappears.
2406
+ const named = () => {
2407
+ const t = toolTarget(c.name, JSON.parse(c.inputFull));
2408
+ const why = c.verdict?.reason ?? c.reason;
2409
+ return why === null || why === undefined || why === "" || why === "interrupted" ? t : `${t} (${why})`;
2410
+ };
2411
+ if (c.verdict?.decision === "denied" || (c.reason !== null && c.reason !== "interrupted" && /denied/i.test(c.reason))) {
2412
+ denied += 1;
2413
+ if (what === "")
2414
+ what = named();
2415
+ }
2416
+ else if (c.reason === "interrupted") {
2417
+ interrupted += 1;
2418
+ }
2419
+ else if (c.isError || c.reason !== null) {
2420
+ failed += 1;
2421
+ if (what === "")
2422
+ what = named();
2423
+ }
2424
+ }
2425
+ const out = [];
2426
+ if (failed > 0)
2427
+ out.push(["failed", failed, what]);
2428
+ if (denied > 0)
2429
+ out.push(["denied", denied, what]);
2430
+ if (interrupted > 0)
2431
+ out.push(["interrupted", interrupted, ""]);
2432
+ return out;
2433
+ }
1965
2434
  #segmentHasTrouble(seg) {
1966
2435
  // R3g (fable, 2026-08-28): a DENIED call is the case this rule
1967
2436
  // exists for, and it was the one case the predicate could not
@@ -2030,6 +2499,11 @@ export class Body {
2030
2499
  const cell = this.#cells[i];
2031
2500
  if (cell.kind !== "thinking" && cell.kind !== "tool")
2032
2501
  return false;
2502
+ // R3i phase 5: an answered question is WORDS (law 1.7). It commits
2503
+ // when it is done, like prose, and is never held for a fold that
2504
+ // is not going to speak for it.
2505
+ if (cell.kind === "tool" && cell.name === "ask_user")
2506
+ return false;
2033
2507
  const turn = cell.turn >= 0 ? this.#turns[cell.turn] : undefined;
2034
2508
  if (turn === undefined || turn !== this.#turns[this.#turns.length - 1])
2035
2509
  return false;
@@ -2052,15 +2526,27 @@ export class Body {
2052
2526
  // The quiet turn is the same rule seen from one side: its single
2053
2527
  // segment never closes until the settle, so it holds exactly as
2054
2528
  // 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
2529
+ // DECLARED SUPERSESSION (R3i phase 3, owner-ruled) THE HOLD IS
2530
+ // THE SEGMENT'S AGAIN.
2531
+ //
2532
+ // R3d had made it the TURN's, so a turn's whole work waited on the
2533
+ // settle and every one of its counts then landed on ONE line above
2534
+ // all of its prose. The shape the owner asked for is one summary
2535
+ // per stretch, standing with the prose that stretch led to — which
2536
+ // requires a stretch to commit when its own text arrives.
2537
+ //
2538
+ // R3d's stated reason for leaving the segment was R3b's disease (a
2539
+ // chatty model turning every call into its own `✦ thought 2s ·
2540
+ // 1 read` row); the cures are the two rules R3b never had — a fold
2541
+ // must absorb at least two rows, and a stretch of exactly one call
2542
+ // names its TARGET rather than its count.
2543
+ //
2544
+ // The force-commit path still overrides this (a stretch too big for
2059
2545
  // the screen spills and renders normally); that is the honest
2060
2546
  // degradation, marked `spilled`.
2061
2547
  const seg = this.#segmentOf(i);
2062
2548
  if (seg !== null)
2063
- return !turn.ended;
2549
+ return seg.closedAt === null;
2064
2550
  // no segment (the pipe path's shape) — W14's original test, kept
2065
2551
  // so a cell that never got a segment behaves as it used to.
2066
2552
  if (!turn.ended && !turn.hasText)
@@ -2154,45 +2640,50 @@ export class Body {
2154
2640
  // The quiet turn keeps its fold because there IS no recap line
2155
2641
  // to carry it: a turn with no text is the fold, and W14's gates
2156
2642
  // 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) {
2643
+ // DECLARED SUPERSESSION (R3i phase 3, owner-ruled) THE FOLD IS
2644
+ // THE SEGMENT'S, AND TROUBLE DOES NOT STOP IT.
2645
+ //
2646
+ // ① the SEGMENT. R3d folded the turn; the owner's shape is one
2647
+ // summary per stretch of work, standing with the prose that
2648
+ // stretch led to. R3b's disease is answered by the two rules
2649
+ // below rather than by leaving the segment: a fold must
2650
+ // absorb at least TWO rows, and a stretch of exactly one
2651
+ // call names its target (see stretchTerms).
2652
+ //
2653
+ // ② TROUBLE. R3b refused to fold any run holding a failure,
2654
+ // and R3g extended that to interrupts. Law 1.3 governs
2655
+ // marks versus WORDS and never granted a failure a
2656
+ // permanent row; law 1.7 says "Work folds, words do not".
2657
+ // So the work folds and the outcome words ride the line —
2658
+ // `1 denied: .env` — and the human sees, without pressing
2659
+ // anything, that trouble happened, on which call, and what
2660
+ // happened. The stderr is behind the key, because it is
2661
+ // detail, not outcome. The cost R3b priced as rare measured
2662
+ // at 2 failures in 28 calls in the 0.16.7 dogfood, with
2663
+ // zero folds as the result.
2664
+ if (turn !== undefined && seg !== null && seg.closedAt !== null && !seg.spilled && seg.cells.length >= 2) {
2665
+ if (!seg.folded) {
2159
2666
  seg.folded = true;
2160
2667
  seg.headCell = i;
2161
2668
  turn.folded = true;
2162
- // A9 (ruling R2, mock A): the user chip rides the fold —
2163
- // but ONLY on a quiet turn, where the fold stands for the
2164
- // whole turn and the chip has nowhere else to be. In a
2165
- // turn WITH text the chip cell commits on its own, so a
2166
- // fold that repeated the words would put the user's line
2167
- // on screen twice. The words take the fold's width budget
2168
- // (turnFold is W-awarethe ONE row never trips
2169
- // invariant ①).
2170
- const quiet = turn.ended && !turn.hasText;
2171
- // R3g (fable, 2026-08-28) — DECLARED SUPERSESSION: both
2172
- // branches read the SAME number now, `thoughtSeconds`,
2173
- // the measure the kernel took and handed to endTurn.
2174
- // The non-quiet branch used to re-derive a wall clock
2175
- // from the segment's opening and print it under the word
2176
- // "thought" — a different quantity wearing the same
2177
- // label: a turn that thought 1s and then ran a 40s shell
2178
- // said "thought 41s". The fold only ever renders after
2179
- // endTurn (the gate below requires `turn.ended`), so the
2180
- // honest number is always available by the time it runs.
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;
2675
+ // DECLARED SUPERSESSION (R3i phase 3)A9 NARROWS: the
2676
+ // fold carries WORK, never the human's words.
2181
2677
  //
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;
2189
- return turnFold({
2190
- words: quiet ? turn.words : "",
2191
- thoughtSeconds: seconds,
2192
- reads: turn.reads,
2193
- edits: turn.edits,
2194
- others: [...turn.others],
2195
- }, W);
2678
+ // A9 put the user's words on the fold as the SGR-7 chip.
2679
+ // Measured under R3i, that prints them twice: the chip
2680
+ // BAND commits on the frame it is pushed (it always has —
2681
+ // `#held` exempts non-thinking/tool cells), so a quiet
2682
+ // turn showed ` x ` on its own row and ` x ` again inside
2683
+ // the fold directly beneath it. The band is the record of
2684
+ // what was asked; this line is the record of what was
2685
+ // done. One fact, one row, each.
2686
+ return stretchLine({ ...this.#stretchTerms(seg), phase: "settled", foldKey: seg.foldKey }, W);
2196
2687
  }
2197
2688
  return [];
2198
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.16.8",
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.16.8"
38
+ "@vincemakes/kiso-tui-cells": "0.18.0"
39
39
  }
40
40
  }