@aragon-agent/cli 0.5.6 → 0.5.9

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.
Files changed (156) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +1429 -1105
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/entry-limits.d.ts +65 -0
  8. package/dist/agent/entry-limits.js +133 -0
  9. package/dist/agent/headless.d.ts +8 -0
  10. package/dist/agent/headless.js +40 -0
  11. package/dist/agent/reducer.d.ts +253 -3
  12. package/dist/agent/reducer.js +553 -28
  13. package/dist/agent/retry-view.d.ts +68 -0
  14. package/dist/agent/retry-view.js +95 -0
  15. package/dist/agent/system-prompt.d.ts +11 -0
  16. package/dist/agent/system-prompt.js +2 -0
  17. package/dist/cli.js +122 -4
  18. package/dist/commands/builtins.d.ts +1 -1
  19. package/dist/commands/builtins.js +313 -2
  20. package/dist/commands/perf.d.ts +45 -0
  21. package/dist/commands/perf.js +78 -0
  22. package/dist/config/cli-commands.d.ts +53 -2
  23. package/dist/config/cli-commands.js +136 -3
  24. package/dist/config/env.d.ts +6 -1
  25. package/dist/config/env.js +131 -1
  26. package/dist/config/load.d.ts +63 -0
  27. package/dist/config/load.js +125 -2
  28. package/dist/config/schema.d.ts +273 -10
  29. package/dist/config/schema.js +250 -7
  30. package/dist/config/store.js +20 -1
  31. package/dist/fast/critique.d.ts +43 -0
  32. package/dist/fast/critique.js +62 -0
  33. package/dist/fast/digest.d.ts +42 -0
  34. package/dist/fast/digest.js +85 -0
  35. package/dist/fast/frames.d.ts +99 -0
  36. package/dist/fast/frames.js +180 -0
  37. package/dist/fast/limits.d.ts +98 -0
  38. package/dist/fast/limits.js +98 -0
  39. package/dist/fast/prompt.d.ts +72 -0
  40. package/dist/fast/prompt.js +94 -0
  41. package/dist/fast/resolve.d.ts +43 -0
  42. package/dist/fast/resolve.js +105 -0
  43. package/dist/fast/review-call.d.ts +99 -0
  44. package/dist/fast/review-call.js +130 -0
  45. package/dist/fast/reviewer.d.ts +248 -0
  46. package/dist/fast/reviewer.js +664 -0
  47. package/dist/fast/types.d.ts +128 -0
  48. package/dist/fast/types.js +14 -0
  49. package/dist/fast/wiring.d.ts +170 -0
  50. package/dist/fast/wiring.js +316 -0
  51. package/dist/logging/install.d.ts +15 -1
  52. package/dist/logging/install.js +37 -4
  53. package/dist/logging/logger.d.ts +1 -1
  54. package/dist/session/persist.js +57 -1
  55. package/dist/team/normalize.d.ts +26 -1
  56. package/dist/team/normalize.js +23 -3
  57. package/dist/team/prompt.d.ts +11 -0
  58. package/dist/team/prompt.js +10 -0
  59. package/dist/team/report.d.ts +18 -0
  60. package/dist/team/report.js +27 -1
  61. package/dist/team/runtime.d.ts +13 -2
  62. package/dist/team/runtime.js +33 -6
  63. package/dist/team/subagent.d.ts +19 -1
  64. package/dist/team/subagent.js +72 -10
  65. package/dist/team/task-tool.d.ts +26 -0
  66. package/dist/team/task-tool.js +48 -4
  67. package/dist/team/types.d.ts +80 -1
  68. package/dist/tools/bash-tool.js +19 -3
  69. package/dist/tools/diff.d.ts +25 -5
  70. package/dist/tools/diff.js +72 -41
  71. package/dist/tools/file-change-store.d.ts +34 -0
  72. package/dist/tools/file-change-store.js +57 -0
  73. package/dist/tools/fs-tools.d.ts +26 -0
  74. package/dist/tools/fs-tools.js +62 -5
  75. package/dist/tools/index.d.ts +31 -0
  76. package/dist/tools/index.js +7 -1
  77. package/dist/tools/patch.d.ts +115 -0
  78. package/dist/tools/patch.js +453 -0
  79. package/dist/tools/terminal-output.d.ts +107 -0
  80. package/dist/tools/terminal-output.js +198 -0
  81. package/dist/tools/tool-output-store.d.ts +68 -0
  82. package/dist/tools/tool-output-store.js +102 -0
  83. package/dist/ui/ActivityLine.d.ts +48 -0
  84. package/dist/ui/ActivityLine.js +22 -0
  85. package/dist/ui/App.d.ts +1 -4
  86. package/dist/ui/App.js +357 -44
  87. package/dist/ui/BottomStatusRow.d.ts +43 -0
  88. package/dist/ui/BottomStatusRow.js +14 -0
  89. package/dist/ui/Composer.d.ts +1 -6
  90. package/dist/ui/Composer.js +2 -2
  91. package/dist/ui/Markdown.d.ts +7 -1
  92. package/dist/ui/Markdown.js +15 -48
  93. package/dist/ui/PromptInput.d.ts +1 -15
  94. package/dist/ui/PromptInput.js +1 -38
  95. package/dist/ui/StatusBar.d.ts +50 -0
  96. package/dist/ui/StatusBar.js +7 -3
  97. package/dist/ui/TeamPanel.d.ts +5 -4
  98. package/dist/ui/TeamPanel.js +1 -1
  99. package/dist/ui/TodoPanel.d.ts +5 -4
  100. package/dist/ui/Transcript.d.ts +80 -5
  101. package/dist/ui/Transcript.js +179 -11
  102. package/dist/ui/activity-phrases.d.ts +33 -0
  103. package/dist/ui/activity-phrases.js +50 -0
  104. package/dist/ui/entries/AssistantEntry.d.ts +38 -1
  105. package/dist/ui/entries/AssistantEntry.js +34 -3
  106. package/dist/ui/entries/DiffView.d.ts +67 -0
  107. package/dist/ui/entries/DiffView.js +103 -0
  108. package/dist/ui/entries/FastCard.d.ts +53 -0
  109. package/dist/ui/entries/FastCard.js +87 -0
  110. package/dist/ui/entries/RetryCard.d.ts +56 -0
  111. package/dist/ui/entries/RetryCard.js +60 -0
  112. package/dist/ui/entries/TeamCard.d.ts +9 -1
  113. package/dist/ui/entries/TeamCard.js +28 -2
  114. package/dist/ui/entries/TodoCard.d.ts +9 -1
  115. package/dist/ui/entries/TodoCard.js +23 -1
  116. package/dist/ui/entries/ToolCard.d.ts +50 -1
  117. package/dist/ui/entries/ToolCard.js +104 -7
  118. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  119. package/dist/ui/entries/UserEntry.d.ts +9 -1
  120. package/dist/ui/entries/UserEntry.js +17 -1
  121. package/dist/ui/exit-snapshot.d.ts +7 -0
  122. package/dist/ui/glyphs.d.ts +9 -0
  123. package/dist/ui/glyphs.js +2 -0
  124. package/dist/ui/layout/AppShell.d.ts +13 -22
  125. package/dist/ui/layout/AppShell.js +3 -38
  126. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  127. package/dist/ui/layout/MeasuredEntry.js +30 -0
  128. package/dist/ui/layout/ScrollViewport.js +8 -2
  129. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  130. package/dist/ui/layout/viewport-geometry.js +27 -0
  131. package/dist/ui/layout/virtual-window.d.ts +100 -0
  132. package/dist/ui/layout/virtual-window.js +308 -0
  133. package/dist/ui/live-clamp.d.ts +42 -0
  134. package/dist/ui/live-clamp.js +74 -0
  135. package/dist/ui/markdown-blocks.d.ts +25 -1
  136. package/dist/ui/markdown-blocks.js +43 -0
  137. package/dist/ui/overlays/HelpOverlay.js +16 -2
  138. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  139. package/dist/ui/overlays/SettingsScreen.js +147 -1
  140. package/dist/ui/render-cache.d.ts +81 -0
  141. package/dist/ui/render-cache.js +195 -0
  142. package/dist/ui/render-governor.d.ts +54 -0
  143. package/dist/ui/render-governor.js +78 -0
  144. package/dist/ui/transcript-text.d.ts +11 -0
  145. package/dist/ui/transcript-text.js +32 -0
  146. package/dist/ui/use-height-store.d.ts +31 -0
  147. package/dist/ui/use-height-store.js +76 -0
  148. package/dist/ui/use-render-governor.d.ts +39 -0
  149. package/dist/ui/use-render-governor.js +57 -0
  150. package/dist/ui/use-startup-notices.d.ts +9 -1
  151. package/dist/ui/use-startup-notices.js +35 -1
  152. package/dist/ui/use-wheel-routing.d.ts +16 -32
  153. package/dist/ui/use-wheel-routing.js +21 -38
  154. package/package.json +2 -2
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Viewport virtualisation maths (tui-render-performance L3 / R1). Pure,
3
+ * React-free, unit-tested.
4
+ *
5
+ * THE STRUCTURAL FIX. Ink's `renderNodeToOutput` has no early-out for clipped
6
+ * subtrees: for every `ink-text` node it runs `squashTextNodes`, `widestLine`
7
+ * and possibly `wrapText` — all O(chars) — and `Output.get()` then evaluates
8
+ * `widestLine` a SECOND time before it is allowed to skip an out-of-clip
9
+ * operation. A clipped 50 000-character entry therefore costs two full string
10
+ * scans per frame and buys nothing. Bounding the ENTRY COUNT (`transcriptWindow`)
11
+ * never bounded that, because one entry can be fifty thousand lines.
12
+ *
13
+ * So entries outside the visible band are not mounted at all. They are replaced
14
+ * by two spacer boxes whose heights come from a height cache, which makes the
15
+ * per-frame cost O(visible rows) instead of O(characters ever produced).
16
+ *
17
+ * V-4 — `offset` counts rows FROM THE BOTTOM (`layout/scroll.ts:1-13`), and that
18
+ * is what makes virtualisation safe here for free: a height correction on an
19
+ * entry ABOVE the viewport changes the content height and therefore
20
+ * `overflowLines`, while `offset` — and hence what the user is looking at — is
21
+ * unchanged. No scroll-anchoring compensation pass is required. Do NOT
22
+ * "simplify" `offset` to count from the top.
23
+ */
24
+ import { FAST_LIMITS } from '../../fast/limits.js';
25
+ import { GUTTER_WIDTH } from './Gutter.js';
26
+ export const VIRTUAL_LIMITS = {
27
+ /**
28
+ * Entries kept mounted on each side of the visible band.
29
+ *
30
+ * V-2 — must stay `>= 2`, so a one-row scroll can never expose an entry whose
31
+ * height has never been measured at the very edge of the viewport.
32
+ */
33
+ overscan: 2,
34
+ /** Height-cache ceiling (LRU). Four thousand numbers, not four thousand strings. */
35
+ heightEntries: 4096,
36
+ };
37
+ /**
38
+ * A cheap content version: it changes exactly when the rendered content does.
39
+ *
40
+ * DERIVED, NEVER HASHED. Hashing a 256 KiB string once per entry per frame would
41
+ * recreate the very cost this module exists to remove, so every field below is
42
+ * an O(1) read of a length or a scalar.
43
+ *
44
+ * I-L3-1 — this must change whenever the rendered output changes. The only lossy
45
+ * case is a same-LENGTH text replacement, which the reducer cannot produce: all
46
+ * text fields are append-only (`appendBounded` only ever elides the middle,
47
+ * which changes the length). A future NON-APPEND mutation MUST extend the
48
+ * revision, or the entry freezes at a stale height AND a stale rendered subtree
49
+ * with nothing anywhere reporting it. `virtual-window.test.ts` enumerates every
50
+ * kind against every mutating action.
51
+ */
52
+ export function entryRevision(entry) {
53
+ switch (entry.kind) {
54
+ case 'user':
55
+ return 'u';
56
+ case 'notice':
57
+ return 'n';
58
+ // `thinkingMs` is O(1) and it CHANGES THE ROW: sealing the clock turns
59
+ // `* thought` into `* thought for 12s`, and with thinking hidden that row is
60
+ // the entry's whole rendered height.
61
+ case 'assistant':
62
+ return `a${entry.text.length}.${entry.thinking?.length ?? 0}.${entry.streaming ? 1 : 0}.${entry.aborted ? 1 : 0}.${entry.thinkingMs ?? -1}`;
63
+ // `patch.lineCount` is PRECOMPUTED for exactly this term: summing
64
+ // `hunks[].lines.length` per entry per frame is the cost the height cache
65
+ // exists to remove, and I-L3-1 forbids hashing.
66
+ // `liveSeq` IS A COUNTER AND NOT `live.length`, and that is precisely the
67
+ // NON-APPEND mutation I-L3-1 above says must extend the revision: a
68
+ // fixed-size tail evicting its oldest row while appending a new one of the
69
+ // same width leaves the joined length unchanged, so a length term would go
70
+ // on matching while the card's content changed under it.
71
+ //
72
+ // SIX TERMS NOW, the longest revision in this file (P2-12). Still O(1) per
73
+ // entry per frame, which is the property I-L3-1 requires -- but the next
74
+ // field makes it seven, and a term that is not O(1) would put the cost this
75
+ // module exists to remove back on every frame.
76
+ case 'tool':
77
+ return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}.${entry.patch?.lineCount ?? -1}.${entry.liveSeq ?? -1}`;
78
+ case 'team':
79
+ return `m${entry.active ? 1 : 0}.${entry.runs.length}.${entry.runs
80
+ .map((r) => r.phase[0])
81
+ .join('')}.${entry.runs.reduce((acc, r) => acc + r.toolCalls, 0)}`;
82
+ case 'todo':
83
+ return `d${entry.live ? 1 : 0}.${entry.doneCount}.${entry.total}.${entry.items.length}`;
84
+ /**
85
+ * The retry card is ONE ROW IN EVERY PHASE, so its revision carries the phase
86
+ * and the counter but deliberately NOT the countdown: `resumeAt` changes the
87
+ * rendered SECONDS once a second while the height stays 1, and folding it in
88
+ * here would invalidate the height cache 30 times per backoff to re-measure a
89
+ * number that cannot change the answer.
90
+ *
91
+ * `phase` and `attempt` ARE in, because they change the row's text and a stale
92
+ * measurement would be wrong in a way nothing reports (I-L3-1).
93
+ */
94
+ case 'retry':
95
+ return `r${entry.phase}.${entry.attempt}.${entry.maxRetries}`;
96
+ /**
97
+ * `status`, `live` and the TEXT LENGTH, because all three change the rows the
98
+ * card draws: `advice` renders a body, everything else renders one line, and
99
+ * the body's length decides how many rows it wraps to. `durationMs` is in the
100
+ * header, which is `wrap="truncate"`, so it can never change the height.
101
+ */
102
+ case 'fast':
103
+ return `f${entry.status}.${entry.live ? 1 : 0}.${entry.text?.length ?? 0}.${entry.detail?.length ?? 0}`;
104
+ default:
105
+ return 'x';
106
+ }
107
+ }
108
+ /**
109
+ * The height-cache key.
110
+ *
111
+ * V-5 — `cols` is part of the key, so a resize invalidates every entry. The
112
+ * frame after a resize renders from estimates and re-measures lazily: one
113
+ * reflow, no crash, no stale layout.
114
+ *
115
+ * `flags` carries the render-affecting switches that are neither per-entry nor
116
+ * part of `density` — today only `thinkingVisible` (Ctrl+T). It is a parameter
117
+ * rather than a fifth positional scalar so a future switch costs one character
118
+ * at the call site instead of a signature change at four.
119
+ */
120
+ export function heightKey(entry, cols, expanded, density, flags = '') {
121
+ return `${entry.id}|${entryRevision(entry)}|${cols}|${expanded ? 1 : 0}|${density}|${flags}`;
122
+ }
123
+ /** Rows a string occupies at `usable` columns, wrapping included. At least 1. */
124
+ function wrappedRows(text, usable) {
125
+ const width = Math.max(1, usable);
126
+ let rows = 0;
127
+ let lineLen = 0;
128
+ for (let i = 0; i < text.length; i += 1) {
129
+ if (text.charCodeAt(i) === 10) {
130
+ rows += Math.max(1, Math.ceil(lineLen / width));
131
+ lineLen = 0;
132
+ }
133
+ else {
134
+ lineLen += 1;
135
+ }
136
+ }
137
+ return rows + Math.max(1, Math.ceil(lineLen / width));
138
+ }
139
+ /** Collapsed preview rows in `ToolCard`; duplicated as a NUMBER, not imported. */
140
+ const TOOL_COLLAPSED_LINES = 8;
141
+ /** Collapsed diff rows in `DiffView`; duplicated for the same reason as above. */
142
+ const DIFF_COLLAPSED_LINES = 12;
143
+ /** Live tail rows in `ToolCard`; duplicated as a NUMBER, for the same reason. */
144
+ const TOOL_LIVE_TAIL_ROWS = 8;
145
+ /**
146
+ * Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
147
+ * exists.
148
+ *
149
+ * Upper bound rather than best guess, and deliberately so: an over-estimate
150
+ * makes the scroll position slightly conservative and self-corrects the frame
151
+ * after the entry is measured, while an under-estimate can put the newest output
152
+ * off the bottom of a viewport the user believes is pinned.
153
+ */
154
+ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisible = true) {
155
+ const usable = Math.max(1, cols - GUTTER_WIDTH);
156
+ // `separationRows` needs the PRECEDING entry, which a per-entry estimate does
157
+ // not have. One row is its maximum, so charging it unconditionally keeps this
158
+ // an upper bound (`compact` never spends it at all).
159
+ const separation = density === 'comfortable' ? 1 : 0;
160
+ switch (entry.kind) {
161
+ case 'user':
162
+ return separation + wrappedRows(entry.text, usable);
163
+ case 'notice':
164
+ return separation + wrappedRows(entry.text, usable);
165
+ case 'assistant': {
166
+ // THE PARAMETER'S `true` DEFAULT IS A FAIL-SAFE FOR A FORGOTTEN CALLER,
167
+ // NOT THE PLAN FOR THE ONE THERE IS (P1-6a). `Transcript.tsx` holds
168
+ // `thinkingVisible` as a prop and MUST pass it: with thinking hidden by
169
+ // default, an unpassed flag inflates every assistant entry that thought by
170
+ // `1 + wrappedRows(thinking)` — hundreds of rows at `thinkingLevel: high`
171
+ // against a real height of one. The self-correction argument does not
172
+ // rescue that, because an entry is only measured once `selectWindow`
173
+ // mounts it, and an entry estimated at 400 rows is exactly the one that
174
+ // never gets mounted.
175
+ const thinking = !entry.thinking
176
+ ? 0
177
+ : thinkingVisible
178
+ ? 1 + wrappedRows(entry.thinking, usable)
179
+ : // The collapsed `thought for Ns` marker is exactly one row, and it is
180
+ // suppressed while the entry is still streaming (the activity line is
181
+ // the live surface then).
182
+ entry.streaming
183
+ ? 0
184
+ : 1;
185
+ const body = entry.text.length > 0 ? wrappedRows(entry.text, usable) : 1;
186
+ return separation + thinking + body + (entry.aborted ? 1 : 0);
187
+ }
188
+ case 'tool': {
189
+ const settled = entry.status === 'done' || entry.status === 'error';
190
+ // WHEN A PATCH IS PRESENT IT, NOT THE PREVIEW, IS WHAT DRAWS (§3.4): the
191
+ // stored preview is one line by then, so estimating from it would report a
192
+ // 3-row card for a 12-row diff — an UNDER-estimate, the direction this
193
+ // file names as unsafe.
194
+ if (settled && entry.patch) {
195
+ const rows = entry.patch.lineCount + Math.max(0, entry.patch.hunks.length - 1);
196
+ const shown = expanded ? rows : Math.min(rows, DIFF_COLLAPSED_LINES);
197
+ // card header + `+N -M` summary + rows + footer
198
+ return separation + 1 + 1 + shown + 1;
199
+ }
200
+ // THE ONE-LINE `if (!entry.preview || !settled)` THIS REPLACES COVERED TWO
201
+ // CASES, and both must survive the split (P1-4): not settled, and settled
202
+ // with nothing to draw (an aborted call, a tool that returned an empty
203
+ // string). A `!settled` block alone would drop the second into
204
+ // `wrappedRows(entry.preview, usable)` with `undefined`.
205
+ if (!settled) {
206
+ const live = entry.live?.length ?? 0;
207
+ if (live === 0)
208
+ return separation + 1;
209
+ // Header + tail rows + the unconditional stall/`(running)` footer row.
210
+ //
211
+ // ROWS ARE COUNTED, NOT WRAPPED, which is legitimate only because every
212
+ // live row is rendered `wrap="truncate"` AND clipped at
213
+ // `LIVE_ROW_MAX_CHARS` on the way into the store -- the same argument
214
+ // `DiffView.tsx:6-11` makes for itself, and the same coupling: if a
215
+ // future change lets a live row wrap, this silently UNDER-counts, the
216
+ // direction this file names as the one the layout cannot absorb.
217
+ return separation + 1 + Math.min(live, TOOL_LIVE_TAIL_ROWS) + 1;
218
+ }
219
+ if (!entry.preview) {
220
+ return separation + 1;
221
+ }
222
+ const lines = wrappedRows(entry.preview, usable);
223
+ const shown = expanded ? lines : Math.min(lines, TOOL_COLLAPSED_LINES);
224
+ // header + body + the `+N lines (Ctrl+O)` / `(Ctrl+O to collapse)` footer
225
+ return separation + 1 + shown + 1;
226
+ }
227
+ case 'team':
228
+ return separation + 1 + entry.runs.length + (expanded ? entry.runs.length * 3 + 1 : 1);
229
+ case 'todo':
230
+ return separation + 1 + entry.items.length;
231
+ // ONE ROW, ALWAYS. The card is `wrap="truncate"` in every phase, so this is an
232
+ // exact height rather than an upper bound.
233
+ case 'retry':
234
+ return separation + 1;
235
+ // ADDING THE CASE TO ONLY ONE OF THIS FILE'S TWO SWITCHES YIELDS A
236
+ // TRANSCRIPT WHOSE SCROLL ARITHMETIC DISAGREES WITH WHAT IT DREW, which
237
+ // manifests as drift rather than as an error (RV-13). Header row, then
238
+ // either the wrapped critique (clamped to `cardTextRows`) or one status
239
+ // line — an upper bound, which is what this function owes its caller.
240
+ case 'fast': {
241
+ const body = entry.status === 'advice' && entry.text
242
+ ? Math.min(FAST_LIMITS.cardTextRows, wrappedRows(entry.text, usable))
243
+ : 1;
244
+ return separation + 1 + body;
245
+ }
246
+ default:
247
+ return separation + 1;
248
+ }
249
+ }
250
+ /** Choose which entries to mount, and how tall the two spacers must be. */
251
+ export function selectWindow(input) {
252
+ const { entries, heightOf } = input;
253
+ const n = entries.length;
254
+ if (n === 0) {
255
+ return { startIndex: 0, endIndex: 0, leadingRows: 0, trailingRows: 0, totalRows: 0 };
256
+ }
257
+ const heights = new Array(n);
258
+ let totalRows = 0;
259
+ for (let i = 0; i < n; i += 1) {
260
+ const h = heightOf(i);
261
+ const rows = Number.isFinite(h) && h > 0 ? Math.floor(h) : 1;
262
+ heights[i] = rows;
263
+ totalRows += rows;
264
+ }
265
+ // A viewport of 0 is the FIRST FRAME, before `measureElement` has run. Falling
266
+ // back to 1 keeps the live tail mounted through it, so the first paint is real
267
+ // content rather than a blank rectangle that only fills in on the second.
268
+ const viewport = Math.max(1, Math.floor(input.viewportRows) || 0);
269
+ const offset = Math.max(0, Math.floor(input.offset) || 0);
270
+ const overscan = Math.max(VIRTUAL_LIMITS.overscan, Math.floor(input.overscan) || 0);
271
+ const bottom = totalRows - offset;
272
+ const top = bottom - viewport;
273
+ let start = n - 1;
274
+ let acc = 0;
275
+ for (let i = 0; i < n; i += 1) {
276
+ const next = acc + heights[i];
277
+ if (next > top) {
278
+ start = i;
279
+ break;
280
+ }
281
+ acc = next;
282
+ }
283
+ let end = n;
284
+ acc = 0;
285
+ for (let i = 0; i < n; i += 1) {
286
+ if (acc >= bottom) {
287
+ end = i;
288
+ break;
289
+ }
290
+ acc += heights[i];
291
+ }
292
+ start = Math.max(0, start - overscan);
293
+ end = Math.min(n, end + overscan);
294
+ // V-3 — while pinned, the LAST entry is always inside the window, whatever its
295
+ // height. The live tail must be real content and never a spacer, or a
296
+ // streaming answer renders as a growing blank rectangle.
297
+ if (offset <= 0)
298
+ end = n;
299
+ if (end <= start)
300
+ end = Math.min(n, start + 1);
301
+ let leadingRows = 0;
302
+ for (let i = 0; i < start; i += 1)
303
+ leadingRows += heights[i];
304
+ let trailingRows = 0;
305
+ for (let i = end; i < n; i += 1)
306
+ trailingRows += heights[i];
307
+ return { startIndex: start, endIndex: end, leadingRows, trailingRows, totalRows };
308
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
3
+ *
4
+ * Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
5
+ * region shorter than the terminal. When one live entry grew taller than
6
+ * `stdout.rows`, Ink took the branch at `ink.js:118-123`:
7
+ *
8
+ * if (outputHeight >= this.options.stdout.rows) {
9
+ * stdout.write(clearTerminal + this.fullStaticOutput + output);
10
+ * }
11
+ *
12
+ * `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
13
+ * is never trimmed, so once that branch is taken it is taken on every subsequent
14
+ * frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
15
+ * and the terminal receives the entire session history thirty times a second.
16
+ * That is a genuine hard freeze, and it is reachable today by any inline user
17
+ * (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
18
+ * writes a long answer.
19
+ *
20
+ * I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
21
+ * branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
22
+ * `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
23
+ * `fullStaticOutput` growth (it is private and append-only); preventing the
24
+ * branch that WRITES it is the entire mitigation, and nothing about the memory
25
+ * it holds is fixed by this module.
26
+ *
27
+ * Nothing is lost, only deferred: when the entry settles it moves into
28
+ * `<Static>` and is printed in full.
29
+ */
30
+ export interface LiveClamp {
31
+ text: string;
32
+ /** Rows dropped off the FRONT. `0` means the text was returned untouched. */
33
+ hiddenRows: number;
34
+ }
35
+ /**
36
+ * Keep the LAST `maxRows` rows of `text`; report how many were dropped.
37
+ *
38
+ * Rows, not wrapped display lines: this runs per frame on a streaming entry, and
39
+ * a wrap-aware count would need the column width and a second full scan of a
40
+ * string this function exists to avoid scanning twice.
41
+ */
42
+ export declare function clampLiveText(text: string, maxRows: number): LiveClamp;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
3
+ *
4
+ * Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
5
+ * region shorter than the terminal. When one live entry grew taller than
6
+ * `stdout.rows`, Ink took the branch at `ink.js:118-123`:
7
+ *
8
+ * if (outputHeight >= this.options.stdout.rows) {
9
+ * stdout.write(clearTerminal + this.fullStaticOutput + output);
10
+ * }
11
+ *
12
+ * `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
13
+ * is never trimmed, so once that branch is taken it is taken on every subsequent
14
+ * frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
15
+ * and the terminal receives the entire session history thirty times a second.
16
+ * That is a genuine hard freeze, and it is reachable today by any inline user
17
+ * (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
18
+ * writes a long answer.
19
+ *
20
+ * I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
21
+ * branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
22
+ * `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
23
+ * `fullStaticOutput` growth (it is private and append-only); preventing the
24
+ * branch that WRITES it is the entire mitigation, and nothing about the memory
25
+ * it holds is fixed by this module.
26
+ *
27
+ * Nothing is lost, only deferred: when the entry settles it moves into
28
+ * `<Static>` and is printed in full.
29
+ */
30
+ /**
31
+ * Keep the LAST `maxRows` rows of `text`; report how many were dropped.
32
+ *
33
+ * Rows, not wrapped display lines: this runs per frame on a streaming entry, and
34
+ * a wrap-aware count would need the column width and a second full scan of a
35
+ * string this function exists to avoid scanning twice.
36
+ */
37
+ export function clampLiveText(text, maxRows) {
38
+ if (!Number.isFinite(maxRows) || maxRows <= 0)
39
+ return { text: '', hiddenRows: rowCount(text) };
40
+ const keep = Math.floor(maxRows);
41
+ // Cheap reject: a string with fewer than `keep` newlines cannot have more than
42
+ // `keep` rows, and this is the path every ordinary entry takes.
43
+ let newlines = 0;
44
+ for (let i = 0; i < text.length; i += 1) {
45
+ if (text.charCodeAt(i) === 10)
46
+ newlines += 1;
47
+ }
48
+ if (newlines + 1 <= keep)
49
+ return { text, hiddenRows: 0 };
50
+ // Walk back from the end to the `keep`-th newline rather than splitting the
51
+ // whole string: the point of the clamp is to stop touching the head at all.
52
+ let cut = text.length;
53
+ let seen = 0;
54
+ for (let i = text.length - 1; i >= 0; i -= 1) {
55
+ if (text.charCodeAt(i) === 10) {
56
+ seen += 1;
57
+ if (seen === keep) {
58
+ cut = i + 1;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ return { text: text.slice(cut), hiddenRows: newlines + 1 - keep };
64
+ }
65
+ function rowCount(text) {
66
+ if (text.length === 0)
67
+ return 0;
68
+ let n = 1;
69
+ for (let i = 0; i < text.length; i += 1) {
70
+ if (text.charCodeAt(i) === 10)
71
+ n += 1;
72
+ }
73
+ return n;
74
+ }
@@ -15,11 +15,13 @@ export interface HeadingBlock {
15
15
  export interface RuleBlock {
16
16
  kind: 'rule';
17
17
  }
18
+ /** Column alignment carried by a table's `|---|:--:|` divider row. */
19
+ export type Align = 'left' | 'right' | 'center';
18
20
  export interface TableBlock {
19
21
  kind: 'table';
20
22
  /** Already column-aligned; render as plain monospace rows. */
21
23
  rows: string[][];
22
- align: ('left' | 'right' | 'center')[];
24
+ align: Align[];
23
25
  }
24
26
  export declare function parseHeading(line: string): HeadingBlock | null;
25
27
  export declare function isRule(line: string): boolean;
@@ -34,6 +36,28 @@ export declare function tableAlignments(dividerLine: string): TableBlock['align'
34
36
  * on any realistic terminal.
35
37
  */
36
38
  export declare function formatTable(rows: string[][], align: TableBlock['align']): string[];
39
+ /**
40
+ * One parsed top-level block.
41
+ *
42
+ * `line` is deliberately UNPARSED: heading / rule / quote / bullet detection is
43
+ * a handful of regexes over one short string and is done at render time, where
44
+ * the theme and the glyph set live. Caching a themed element instead would make
45
+ * `/theme` a no-op until the cache evicted (K-5).
46
+ */
47
+ export type MdBlock = {
48
+ kind: 'code';
49
+ code: string;
50
+ lang: string;
51
+ } | {
52
+ kind: 'table';
53
+ rows: string[][];
54
+ align: Align[];
55
+ } | {
56
+ kind: 'line';
57
+ text: string;
58
+ };
59
+ /** Split a document into fenced code, pipe tables, and everything else. */
60
+ export declare function parseMarkdownBlocks(text: string): MdBlock[];
37
61
  export type InlineSpan = {
38
62
  kind: 'text';
39
63
  text: string;
@@ -73,6 +73,49 @@ export function formatTable(rows, align) {
73
73
  .join(' ')
74
74
  .trimEnd());
75
75
  }
76
+ /** A fence line: ```` ```lang ```` or a bare ```` ``` ````. */
77
+ const FENCE_RE = /^```(\w*)\s*$/;
78
+ /** Split a document into fenced code, pipe tables, and everything else. */
79
+ export function parseMarkdownBlocks(text) {
80
+ const lines = text.split('\n');
81
+ const blocks = [];
82
+ let i = 0;
83
+ while (i < lines.length) {
84
+ const line = lines[i] ?? '';
85
+ const fence = FENCE_RE.exec(line.trim());
86
+ if (fence) {
87
+ const lang = fence[1] || '';
88
+ const codeLines = [];
89
+ i += 1;
90
+ while (i < lines.length && !FENCE_RE.test((lines[i] ?? '').trim())) {
91
+ codeLines.push(lines[i] ?? '');
92
+ i += 1;
93
+ }
94
+ i += 1; // skip the closing fence
95
+ blocks.push({ kind: 'code', code: codeLines.join('\n'), lang });
96
+ continue;
97
+ }
98
+ // A pipe row followed by a divider row starts a table; consume the run.
99
+ const headerCells = parseTableRow(line);
100
+ if (headerCells && i + 1 < lines.length && isTableDivider(lines[i + 1] ?? '')) {
101
+ const align = tableAlignments(lines[i + 1] ?? '');
102
+ const rows = [headerCells];
103
+ i += 2;
104
+ while (i < lines.length) {
105
+ const cells = parseTableRow(lines[i] ?? '');
106
+ if (!cells)
107
+ break;
108
+ rows.push(cells);
109
+ i += 1;
110
+ }
111
+ blocks.push({ kind: 'table', rows, align });
112
+ continue;
113
+ }
114
+ blocks.push({ kind: 'line', text: line });
115
+ i += 1;
116
+ }
117
+ return blocks;
118
+ }
76
119
  const CODE_RE = /`[^`]+`/g;
77
120
  const EMPHASIS_RE = /(\*\*[^*]+\*\*|~~[^~]+~~|\*[^*]+\*)/g;
78
121
  /**
@@ -10,10 +10,10 @@ function keyRows(times) {
10
10
  ['Shift+Tab', 'Toggle plan mode (same as /plan)'],
11
11
  [`Ctrl+C ${times}2`, 'Exit'],
12
12
  ['Ctrl+L', 'Redraw the frame'],
13
- ['Ctrl+T', 'Toggle thinking blocks'],
13
+ ['Ctrl+T', 'Show/hide thinking (off by default)'],
14
14
  ['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
15
15
  ['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
16
- ['Wheel', 'Scroll the transcript (over the input box: prompt history)'],
16
+ ['Wheel', 'Scroll the transcript (or the open overlay)'],
17
17
  ['Shift+Wheel', 'Scroll the transcript a page'],
18
18
  ['Up / Down', 'Prompt history (empty input)'],
19
19
  ];
@@ -27,6 +27,7 @@ const COMMANDS = [
27
27
  ['/thinking <level>', 'Set thinking level'],
28
28
  ['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
29
29
  ['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
30
+ ['/fast [on|off|model|review]', 'Fast model tier: status, switch, model, review cadence'],
30
31
  [
31
32
  '/todo [on|off|panel|follow|clear|continue]',
32
33
  'Todo planning: status, switches, the rail, follow-through',
@@ -67,6 +68,16 @@ const TEAM_ROWS = [
67
68
  ['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
68
69
  ['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
69
70
  ];
71
+ const FAST_ROWS = [
72
+ ['/fast', 'Status: on/off, the model, the cadence, and this session\'s totals'],
73
+ ['/fast on | off', 'Switch the tier for this session (and save it)'],
74
+ ['/fast model <id>', 'Set the fast model; accepts provider:model'],
75
+ ['/fast same', 'Run the fast tier on the main model'],
76
+ ['/fast review <n> | off', 'Turns between automatic reviews, or turn them off'],
77
+ ['/fast delegate on | off', 'Allow model:"fast" on task subagents'],
78
+ ['--fast / --no-fast', 'Start a session with the tier on or off (also ARAGON_FAST=1)'],
79
+ ['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
80
+ ];
70
81
  /** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
71
82
  export function helpRows(theme, caps) {
72
83
  const glyphs = pickGlyphs(caps);
@@ -85,6 +96,9 @@ export function helpRows(theme, caps) {
85
96
  rows.push(section('Team subagents'));
86
97
  for (const [c, d] of TEAM_ROWS)
87
98
  rows.push(pair(c, d));
99
+ rows.push(section('Fast model tier'));
100
+ for (const [c, d] of FAST_ROWS)
101
+ rows.push(pair(c, d));
88
102
  rows.push(section('Skills'));
89
103
  for (const [c, d] of SKILL_ROWS)
90
104
  rows.push(pair(c, d));