@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.
@@ -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, 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
- #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;
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
- const idx = this.#collapsed[this.#expandPtr % this.#collapsed.length];
1049
- 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);
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
- `${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
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 · ctrl+r opens the next fold${p.reset}`,
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's ONE line, drawn once, at its first cell
1698
- if (!stretchDrawn) {
1699
- stretchDrawn = true;
1700
- const rows = stretchLine({ ...this.#stretchTerms(openSeg), phase: this.#stretchPhase(openSeg), mark: twinkleFrame(this.#spinnerI) }, W);
1701
- out.push(...this.#space(i, prev, rows));
1702
- prev = rows;
1703
- }
1704
- // a DONE cell's row is gone; its count is on the line above.
1705
- // A cell still in flight keeps its row and its output —
1706
- // hiding the work in flight would be the opposite defect.
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
- if (runningShown >= LIVE_ACT_HEADS) {
1711
- runningHidden += 1;
1712
- continue;
1713
- }
1714
- runningShown += 1;
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
- let lines = 0;
1802
- let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
1803
- for (let i = this.#committed; i < this.#cells.length; i += 1) {
1804
- const rows = cellComponent(this.#cells[i]).render(W, ctx);
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.17.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.17.0"
38
+ "@vincemakes/kiso-tui-cells": "0.18.0"
39
39
  }
40
40
  }