@aragon-agent/cli 0.5.8 → 0.5.10

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 +1157 -747
  2. package/README.md +1552 -1163
  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/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/boot/guard.d.ts +135 -0
  16. package/dist/boot/guard.js +218 -0
  17. package/dist/boot/rollback.d.ts +86 -0
  18. package/dist/boot/rollback.js +185 -0
  19. package/dist/cli.js +368 -3
  20. package/dist/commands/builtins.d.ts +1 -1
  21. package/dist/commands/builtins.js +426 -2
  22. package/dist/commands/registry.d.ts +17 -0
  23. package/dist/config/app-paths.d.ts +31 -0
  24. package/dist/config/app-paths.js +37 -0
  25. package/dist/config/cli-commands.d.ts +79 -2
  26. package/dist/config/cli-commands.js +185 -3
  27. package/dist/config/env.d.ts +15 -3
  28. package/dist/config/env.js +143 -3
  29. package/dist/config/load.d.ts +66 -0
  30. package/dist/config/load.js +107 -1
  31. package/dist/config/schema.d.ts +268 -2
  32. package/dist/config/schema.js +288 -1
  33. package/dist/config/store.js +32 -1
  34. package/dist/fast/critique.d.ts +43 -0
  35. package/dist/fast/critique.js +62 -0
  36. package/dist/fast/digest.d.ts +42 -0
  37. package/dist/fast/digest.js +85 -0
  38. package/dist/fast/frames.d.ts +99 -0
  39. package/dist/fast/frames.js +180 -0
  40. package/dist/fast/limits.d.ts +98 -0
  41. package/dist/fast/limits.js +98 -0
  42. package/dist/fast/prompt.d.ts +72 -0
  43. package/dist/fast/prompt.js +94 -0
  44. package/dist/fast/resolve.d.ts +43 -0
  45. package/dist/fast/resolve.js +105 -0
  46. package/dist/fast/review-call.d.ts +99 -0
  47. package/dist/fast/review-call.js +130 -0
  48. package/dist/fast/reviewer.d.ts +248 -0
  49. package/dist/fast/reviewer.js +664 -0
  50. package/dist/fast/types.d.ts +128 -0
  51. package/dist/fast/types.js +14 -0
  52. package/dist/fast/wiring.d.ts +170 -0
  53. package/dist/fast/wiring.js +316 -0
  54. package/dist/launcher.d.ts +14 -0
  55. package/dist/launcher.js +43 -0
  56. package/dist/logging/install.d.ts +15 -1
  57. package/dist/logging/install.js +37 -4
  58. package/dist/logging/logger.d.ts +1 -1
  59. package/dist/session/persist.js +57 -1
  60. package/dist/team/normalize.d.ts +26 -1
  61. package/dist/team/normalize.js +23 -3
  62. package/dist/team/prompt.d.ts +11 -0
  63. package/dist/team/prompt.js +10 -0
  64. package/dist/team/report.d.ts +18 -0
  65. package/dist/team/report.js +27 -1
  66. package/dist/team/runtime.d.ts +13 -2
  67. package/dist/team/runtime.js +33 -6
  68. package/dist/team/subagent.d.ts +19 -1
  69. package/dist/team/subagent.js +72 -10
  70. package/dist/team/task-tool.d.ts +26 -0
  71. package/dist/team/task-tool.js +48 -4
  72. package/dist/team/types.d.ts +80 -1
  73. package/dist/tools/bash-tool.js +19 -3
  74. package/dist/tools/diff.d.ts +25 -5
  75. package/dist/tools/diff.js +72 -41
  76. package/dist/tools/file-change-store.d.ts +34 -0
  77. package/dist/tools/file-change-store.js +57 -0
  78. package/dist/tools/fs-tools.d.ts +26 -0
  79. package/dist/tools/fs-tools.js +62 -5
  80. package/dist/tools/index.d.ts +31 -0
  81. package/dist/tools/index.js +7 -1
  82. package/dist/tools/patch.d.ts +115 -0
  83. package/dist/tools/patch.js +453 -0
  84. package/dist/tools/terminal-output.d.ts +107 -0
  85. package/dist/tools/terminal-output.js +198 -0
  86. package/dist/tools/tool-output-store.d.ts +68 -0
  87. package/dist/tools/tool-output-store.js +102 -0
  88. package/dist/ui/ActivityLine.d.ts +48 -0
  89. package/dist/ui/ActivityLine.js +22 -0
  90. package/dist/ui/App.d.ts +11 -4
  91. package/dist/ui/App.js +359 -37
  92. package/dist/ui/BottomStatusRow.d.ts +65 -0
  93. package/dist/ui/BottomStatusRow.js +17 -0
  94. package/dist/ui/Composer.d.ts +1 -6
  95. package/dist/ui/Composer.js +2 -2
  96. package/dist/ui/PromptInput.d.ts +1 -15
  97. package/dist/ui/PromptInput.js +1 -38
  98. package/dist/ui/StatusBar.d.ts +37 -0
  99. package/dist/ui/StatusBar.js +5 -3
  100. package/dist/ui/TeamPanel.d.ts +5 -4
  101. package/dist/ui/TeamPanel.js +1 -1
  102. package/dist/ui/TodoPanel.d.ts +5 -4
  103. package/dist/ui/Transcript.d.ts +26 -3
  104. package/dist/ui/Transcript.js +76 -10
  105. package/dist/ui/UpdateLine.d.ts +44 -0
  106. package/dist/ui/UpdateLine.js +129 -0
  107. package/dist/ui/activity-phrases.d.ts +33 -0
  108. package/dist/ui/activity-phrases.js +50 -0
  109. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  110. package/dist/ui/entries/AssistantEntry.js +3 -2
  111. package/dist/ui/entries/DiffView.d.ts +67 -0
  112. package/dist/ui/entries/DiffView.js +103 -0
  113. package/dist/ui/entries/FastCard.d.ts +53 -0
  114. package/dist/ui/entries/FastCard.js +87 -0
  115. package/dist/ui/entries/RetryCard.d.ts +56 -0
  116. package/dist/ui/entries/RetryCard.js +60 -0
  117. package/dist/ui/entries/TeamCard.js +6 -1
  118. package/dist/ui/entries/ToolCard.d.ts +38 -1
  119. package/dist/ui/entries/ToolCard.js +68 -5
  120. package/dist/ui/glyphs.d.ts +9 -0
  121. package/dist/ui/glyphs.js +2 -0
  122. package/dist/ui/layout/AppShell.d.ts +13 -22
  123. package/dist/ui/layout/AppShell.js +3 -38
  124. package/dist/ui/layout/virtual-window.d.ts +1 -1
  125. package/dist/ui/layout/virtual-window.js +109 -5
  126. package/dist/ui/overlays/HelpOverlay.js +16 -2
  127. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  128. package/dist/ui/overlays/SettingsScreen.js +147 -1
  129. package/dist/ui/transcript-text.js +27 -0
  130. package/dist/ui/use-wheel-routing.d.ts +16 -32
  131. package/dist/ui/use-wheel-routing.js +21 -38
  132. package/dist/update/classify-failure.d.ts +32 -0
  133. package/dist/update/classify-failure.js +60 -0
  134. package/dist/update/install-lock.d.ts +55 -0
  135. package/dist/update/install-lock.js +184 -0
  136. package/dist/update/install-source.d.ts +97 -0
  137. package/dist/update/install-source.js +218 -0
  138. package/dist/update/installer.d.ts +108 -0
  139. package/dist/update/installer.js +242 -0
  140. package/dist/update/limits.d.ts +111 -0
  141. package/dist/update/limits.js +111 -0
  142. package/dist/update/npm-view.d.ts +62 -0
  143. package/dist/update/npm-view.js +133 -0
  144. package/dist/update/registry.d.ts +52 -0
  145. package/dist/update/registry.js +161 -0
  146. package/dist/update/semver.d.ts +89 -0
  147. package/dist/update/semver.js +295 -0
  148. package/dist/update/service.d.ts +193 -0
  149. package/dist/update/service.js +636 -0
  150. package/dist/update/state.d.ts +103 -0
  151. package/dist/update/state.js +142 -0
  152. package/dist/update/types.d.ts +169 -0
  153. package/dist/update/types.js +68 -0
  154. package/package.json +3 -3
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Raw child-process output to terminal-safe rows (agent-activity-presentation-live
3
+ * L2 / §3.2).
4
+ *
5
+ * NOTHING ELSE IN THIS TREE STRIPS ANSI (E-13). `renderBash` hands each line to
6
+ * `<Text>` unmodified (`ui/entries/ToolPreview.tsx:180`), and that survives only
7
+ * because the settled preview is printed ONCE, after the command has stopped
8
+ * writing. Redraw the same text every frame while the child is still emitting
9
+ * `\r`-rewritten progress bars and `\x1b[A` cursor moves and it stops being
10
+ * survivable: the terminal honours cursor motion Ink does not know it emitted,
11
+ * and `wrappedRows` measures escape bytes as if they were columns.
12
+ *
13
+ * So this module is the boundary. It runs ONCE, inside the store's `append`
14
+ * (§3.1.1), and every reader downstream is already safe.
15
+ *
16
+ * Pure, ASCII-only, React-free, Ink-free, and TOTAL: `sanitizeChunk` never
17
+ * throws, for any input including lone surrogates, NUL bytes, a chunk consisting
18
+ * solely of `\x1b`, and a 4 MB chunk containing no `\n` at all.
19
+ *
20
+ * D-27 — IT STRIPS, IT DOES NOT INTERPRET. Keeping colour would mean validating
21
+ * it (an unterminated SGR leaks its attribute into the rest of the frame), and
22
+ * interpreting cursor motion would mean owning a screen buffer. Neither is a
23
+ * presentation round's job.
24
+ */
25
+ /** Columns a live row may occupy before it is clipped. */
26
+ export const LIVE_ROW_MAX_CHARS = 200;
27
+ /** Tab stop width, matching every terminal's default. */
28
+ export const TAB_WIDTH = 8;
29
+ /**
30
+ * The in-progress line's ceiling before it is flushed as a row (§3.2 step 7).
31
+ *
32
+ * AFTER STEP 4 THIS IS A RARE PATH RATHER THAN THE PROGRESS-BAR PATH (D-33). A
33
+ * `\r`-rewritten bar collapses to one row however many times it is redrawn, so
34
+ * reaching 400 characters with no `\n` requires a line that is genuinely that
35
+ * long -- `cat` on a minified bundle. Without the bound the carry is an
36
+ * unbounded accumulator.
37
+ */
38
+ export const CARRY_MAX_CHARS = LIVE_ROW_MAX_CHARS * 2;
39
+ /**
40
+ * Appended to a row clipped at `LIVE_ROW_MAX_CHARS`.
41
+ *
42
+ * SPELLED HERE RATHER THAN IMPORTED from `agent/reducer.ts`, the rule
43
+ * `patch.ts:95-103` already states for the identical string: `tools/` keeps no
44
+ * runtime dependency on the view model for three characters, and
45
+ * `terminal-output.test.ts` asserts the spellings are equal so the pair cannot
46
+ * drift silently.
47
+ */
48
+ export const TRUNCATION_MARK = '...';
49
+ /**
50
+ * How far back a trailing incomplete escape may start.
51
+ *
52
+ * A chunk can split mid-escape, and the next chunk completes it -- so the tail
53
+ * of an unfinished sequence must survive in the carry rather than being stripped
54
+ * down to its visible remainder (`\x1b[` would otherwise render as a bare `[`).
55
+ * The lookback is bounded because an "escape" that has run for 64 characters
56
+ * without a final byte is not an escape, it is a byte that happened to be 0x1b,
57
+ * and holding it forever would pin the carry.
58
+ */
59
+ const MAX_PARTIAL_ESCAPE = 64;
60
+ /** CSI: `\x1b[` params intermediates final. */
61
+ const CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
62
+ /** OSC: `\x1b]` ... terminated by BEL or ST. */
63
+ const OSC = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
64
+ /** The two-byte forms: `\x1b` followed by one character. */
65
+ const ESC_PAIR = /\x1b[\s\S]/g;
66
+ /** C1, plus every C0 control except `\t` `\n` `\r`. */
67
+ const CONTROLS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
68
+ /**
69
+ * Remove every escape sequence and control byte except `\t` `\n` `\r`.
70
+ *
71
+ * Exported for the tests, and used by `toDisplayRow` as the second line of
72
+ * defence over a carry that ends mid-escape.
73
+ */
74
+ export function stripAnsi(text) {
75
+ return text.replace(CSI, '').replace(OSC, '').replace(ESC_PAIR, '').replace(CONTROLS, '');
76
+ }
77
+ /**
78
+ * Index at which a trailing INCOMPLETE escape begins, or `-1`.
79
+ *
80
+ * Only the last `\x1b` can be incomplete: anything before it is either a
81
+ * complete sequence (the regexes above consume it) or a stray byte the control
82
+ * strip removes.
83
+ */
84
+ function partialEscapeStart(text) {
85
+ const at = text.lastIndexOf('\x1b');
86
+ if (at === -1 || text.length - at > MAX_PARTIAL_ESCAPE)
87
+ return -1;
88
+ const rest = text.slice(at);
89
+ // A lone ESC at the very end: the next chunk decides what it is.
90
+ if (rest.length === 1)
91
+ return at;
92
+ // An opened CSI/OSC is incomplete exactly when its own regex does not match
93
+ // from here -- `\x1b[1;3` has no final byte yet, `\x1b]0;title` no terminator.
94
+ if (rest[1] === '[')
95
+ return /^\x1b\[[0-?]*[ -/]*[@-~]/.test(rest) ? -1 : at;
96
+ if (rest[1] === ']')
97
+ return /^\x1b\][\s\S]*?(?:\x07|\x1b\\)/.test(rest) ? -1 : at;
98
+ // Any other two-byte form is already complete.
99
+ return -1;
100
+ }
101
+ /**
102
+ * Apply `\r` as the LINE REWRITE it is (§3.2 step 4 / D-33).
103
+ *
104
+ * A piece containing `\r` is a sequence of overwrites, so the later segment
105
+ * replaces the earlier one's PREFIX and anything the earlier one had beyond it
106
+ * survives -- which is what a real terminal shows, and what keeps R-6's classic
107
+ * `\r`-only tooling from losing content. A thousand progress-bar rewrites
108
+ * collapse to one row holding the final state.
109
+ */
110
+ export function applyCarriageReturns(piece) {
111
+ if (piece.indexOf('\r') === -1)
112
+ return piece;
113
+ return piece.split('\r').reduce((acc, s) => s + acc.slice(s.length), '');
114
+ }
115
+ /** Expand tabs to the next multiple of `TAB_WIDTH`. */
116
+ export function expandTabs(row) {
117
+ if (row.indexOf('\t') === -1)
118
+ return row;
119
+ let out = '';
120
+ for (let i = 0; i < row.length; i += 1) {
121
+ const ch = row[i];
122
+ if (ch === '\t')
123
+ out += ' '.repeat(TAB_WIDTH - (out.length % TAB_WIDTH));
124
+ else
125
+ out += ch;
126
+ }
127
+ return out;
128
+ }
129
+ /** Clip a row at `LIVE_ROW_MAX_CHARS`, marking the cut. */
130
+ export function clipRow(row) {
131
+ return row.length <= LIVE_ROW_MAX_CHARS
132
+ ? row
133
+ : `${row.slice(0, LIVE_ROW_MAX_CHARS)}${TRUNCATION_MARK}`;
134
+ }
135
+ /**
136
+ * The carry as a RENDERED row: the held-back incomplete escape removed, then
137
+ * stripped and clipped.
138
+ *
139
+ * THE PARTIAL MUST BE CUT OFF, NOT STRIPPED (AC-24). `stripAnsi` alone is not
140
+ * enough and fails in the visible direction: its two-byte rule consumes the
141
+ * `\x1b[` of an unfinished CSI and leaves the PARAMETERS behind, so a chunk that
142
+ * split inside `\x1b[31m` renders as `red 3`. The bytes are held for the next
143
+ * chunk to complete; until then they are not text.
144
+ */
145
+ export function toDisplayRow(carry) {
146
+ const at = partialEscapeStart(carry);
147
+ return clipRow(stripAnsi(at === -1 ? carry : carry.slice(0, at)));
148
+ }
149
+ /**
150
+ * Tab expansion is column arithmetic, so it cannot be done after the clip -- but
151
+ * it also need not run over four megabytes to decide the first 200 columns.
152
+ * Every character occupies at least one column, so a `CARRY_MAX_CHARS` prefix
153
+ * covers every column the clip can keep.
154
+ */
155
+ function expandTabsForDisplay(piece) {
156
+ return piece.length <= CARRY_MAX_CHARS
157
+ ? expandTabs(piece)
158
+ : expandTabs(piece.slice(0, CARRY_MAX_CHARS));
159
+ }
160
+ /**
161
+ * Turn a raw chunk into complete rows plus the in-progress line.
162
+ *
163
+ * The order of the steps IS the specification (§3.2), and step 4's "every piece,
164
+ * THE INCOMPLETE TAIL INCLUDED" is the whole of P0-1: a progress bar emits no
165
+ * `\n` for its entire run, so a rule that collapsed only completed lines would
166
+ * never run for npm, pip, curl, docker or any other `\r` writer -- the tail
167
+ * would stay empty, `showLive` false, and the card the single `running` row this
168
+ * round exists to replace.
169
+ */
170
+ export function sanitizeChunk(carry, chunk) {
171
+ const text = (typeof carry === 'string' ? carry : '') + (typeof chunk === 'string' ? chunk : '');
172
+ if (text.length === 0)
173
+ return { rows: [], carry: '' };
174
+ // 1-2. Concatenate and strip, holding back an escape the next chunk finishes.
175
+ const partialAt = partialEscapeStart(text);
176
+ const held = partialAt === -1 ? '' : text.slice(partialAt);
177
+ const clean = stripAnsi(partialAt === -1 ? text : text.slice(0, partialAt));
178
+ // 3. Split: the first n-1 pieces are complete rows, the last is the tail.
179
+ const pieces = clean.split('\n');
180
+ const rows = [];
181
+ for (let i = 0; i < pieces.length - 1; i += 1) {
182
+ // 4-6. Collapse, expand, clip.
183
+ rows.push(clipRow(expandTabsForDisplay(applyCarriageReturns(pieces[i]))));
184
+ }
185
+ const rawTail = applyCarriageReturns(pieces[pieces.length - 1]);
186
+ const tail = expandTabsForDisplay(rawTail);
187
+ // 7. Bound the carry. The held escape survives the flush: it belongs to the
188
+ // NEXT row, and dropping it here would corrupt the sequence it opened.
189
+ //
190
+ // BOTH LENGTHS ARE TESTED, and testing only the expanded one is the bug:
191
+ // `expandTabsForDisplay` truncates its input at `CARRY_MAX_CHARS`, so a
192
+ // megabyte-long line would sit at exactly the bound forever and never flush.
193
+ if (rawTail.length > CARRY_MAX_CHARS || tail.length > CARRY_MAX_CHARS) {
194
+ rows.push(clipRow(tail));
195
+ return { rows, carry: held };
196
+ }
197
+ return { rows, carry: tail + held };
198
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The CLI-local side channel a running tool's output travels on
3
+ * (agent-activity-presentation-live L1 / §3.1.1).
4
+ *
5
+ * It mirrors `file-change-store.ts` in shape and in its documented reasoning,
6
+ * with ONE deliberate difference: it is PEEK-AND-CLEAR, not take-once, because a
7
+ * running command is read many times and consumed once.
8
+ *
9
+ * Four properties, each load-bearing, each with the failure it prevents:
10
+ *
11
+ * - **Owner-scoped.** The key is `${owner}:${toolCallId}`, for D-13's reason
12
+ * unchanged: only `'lead'` is written this round, but subagents build their
13
+ * tools from the same factory, and a child's tool-call id must never be able
14
+ * to paint a lead's card.
15
+ * - **Sanitised on write.** `append` calls `sanitizeChunk` BEFORE storing, so
16
+ * the store's contract is "terminal-safe rows" and no consumer can forget.
17
+ * - **Bounded twice.** `LIVE_TAIL_ROWS` per call and `TOOL_OUTPUT_STORE_CAP`
18
+ * calls. A command emitting 400 MB to stdout costs a fixed number of retained
19
+ * rows: the per-call bound is what makes the DISPLAY bounded, the cap is what
20
+ * makes a session that never settles a call bounded.
21
+ * - **Cleared at settle, not at read.** The authoritative result arrives with
22
+ * `tool_execution_end`; from that instant the tail is superseded and must be
23
+ * released. Clearing there keeps the store's size a function of CONCURRENT
24
+ * calls rather than of total calls.
25
+ */
26
+ /**
27
+ * ROWS retained per tool call.
28
+ *
29
+ * THIS AND THE CAP BELOW ARE DELIBERATELY DIFFERENT NUMBERS (P2-8). They bound
30
+ * unrelated things -- rows inside one call, and calls inside one session -- and
31
+ * a shared literal is how a later edit collapses two bounds into one.
32
+ */
33
+ export declare const LIVE_TAIL_ROWS = 8;
34
+ /** CONCURRENT CALLS retained before the oldest is evicted. */
35
+ export declare const TOOL_OUTPUT_STORE_CAP = 16;
36
+ /**
37
+ * Silence after which the card stops claiming progress and says so (§3.3.4).
38
+ *
39
+ * It lives here rather than in `ToolCard` because it is the STORE's notion of
40
+ * "quiet": the card reads it so there is no second copy to drift.
41
+ */
42
+ export declare const STALL_AFTER_MS = 10000;
43
+ export interface ToolOutputStore {
44
+ /**
45
+ * Append a raw chunk. Sanitises (§3.2), then returns the display tail: the
46
+ * last `LIVE_TAIL_ROWS` of `[...completedRows, carry]`, `carry` last and
47
+ * included only when non-empty.
48
+ */
49
+ append(owner: string, toolCallId: string, chunk: string): readonly string[];
50
+ /** Current tail, or `undefined`. Does NOT remove. Test-facing (P2-2). */
51
+ peek(owner: string, toolCallId: string): readonly string[] | undefined;
52
+ /** Drop the entry -- called at `tool_execution_end`. */
53
+ clear(owner: string, toolCallId: string): void;
54
+ size(): number;
55
+ }
56
+ /**
57
+ * What the controller's fourth CLI-local emitter carries (§3.1.3).
58
+ *
59
+ * `rows` IS THE WHOLE TAIL, not an increment, and that is what lets
60
+ * `mergeDeltas` keep only the last one per `toolCallId` (D-30). It is frozen, so
61
+ * a listener cannot mutate the array a later listener will see.
62
+ */
63
+ export interface ToolOutputEvent {
64
+ toolCallId: string;
65
+ rows: readonly string[];
66
+ }
67
+ export type ToolOutputListener = (event: ToolOutputEvent) => void;
68
+ export declare function createToolOutputStore(cap?: number): ToolOutputStore;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The CLI-local side channel a running tool's output travels on
3
+ * (agent-activity-presentation-live L1 / §3.1.1).
4
+ *
5
+ * It mirrors `file-change-store.ts` in shape and in its documented reasoning,
6
+ * with ONE deliberate difference: it is PEEK-AND-CLEAR, not take-once, because a
7
+ * running command is read many times and consumed once.
8
+ *
9
+ * Four properties, each load-bearing, each with the failure it prevents:
10
+ *
11
+ * - **Owner-scoped.** The key is `${owner}:${toolCallId}`, for D-13's reason
12
+ * unchanged: only `'lead'` is written this round, but subagents build their
13
+ * tools from the same factory, and a child's tool-call id must never be able
14
+ * to paint a lead's card.
15
+ * - **Sanitised on write.** `append` calls `sanitizeChunk` BEFORE storing, so
16
+ * the store's contract is "terminal-safe rows" and no consumer can forget.
17
+ * - **Bounded twice.** `LIVE_TAIL_ROWS` per call and `TOOL_OUTPUT_STORE_CAP`
18
+ * calls. A command emitting 400 MB to stdout costs a fixed number of retained
19
+ * rows: the per-call bound is what makes the DISPLAY bounded, the cap is what
20
+ * makes a session that never settles a call bounded.
21
+ * - **Cleared at settle, not at read.** The authoritative result arrives with
22
+ * `tool_execution_end`; from that instant the tail is superseded and must be
23
+ * released. Clearing there keeps the store's size a function of CONCURRENT
24
+ * calls rather than of total calls.
25
+ */
26
+ import { sanitizeChunk, toDisplayRow } from './terminal-output.js';
27
+ /**
28
+ * ROWS retained per tool call.
29
+ *
30
+ * THIS AND THE CAP BELOW ARE DELIBERATELY DIFFERENT NUMBERS (P2-8). They bound
31
+ * unrelated things -- rows inside one call, and calls inside one session -- and
32
+ * a shared literal is how a later edit collapses two bounds into one.
33
+ */
34
+ export const LIVE_TAIL_ROWS = 8;
35
+ /** CONCURRENT CALLS retained before the oldest is evicted. */
36
+ export const TOOL_OUTPUT_STORE_CAP = 16;
37
+ /**
38
+ * Silence after which the card stops claiming progress and says so (§3.3.4).
39
+ *
40
+ * It lives here rather than in `ToolCard` because it is the STORE's notion of
41
+ * "quiet": the card reads it so there is no second copy to drift.
42
+ */
43
+ export const STALL_AFTER_MS = 10_000;
44
+ const EMPTY = Object.freeze([]);
45
+ export function createToolOutputStore(cap = TOOL_OUTPUT_STORE_CAP) {
46
+ // A `Map` iterates in insertion order, so the first key IS the oldest.
47
+ const entries = new Map();
48
+ const limit = Math.max(1, Math.floor(cap));
49
+ const keyOf = (owner, toolCallId) => `${owner}:${toolCallId}`;
50
+ /**
51
+ * The tail a consumer sees.
52
+ *
53
+ * THE IN-PROGRESS LINE IS PART OF IT, and that is the display half of P0-1: a
54
+ * command whose output is one continuously-rewritten line -- every progress
55
+ * bar there is -- has NO completed rows until it exits, and a tail built from
56
+ * completed rows alone would be empty for its entire run.
57
+ */
58
+ const tailOf = (slot) => {
59
+ const live = toDisplayRow(slot.carry);
60
+ const rows = live.length > 0 ? [...slot.rows, live] : [...slot.rows];
61
+ return Object.freeze(rows.length > LIVE_TAIL_ROWS ? rows.slice(-LIVE_TAIL_ROWS) : rows);
62
+ };
63
+ return {
64
+ append(owner, toolCallId, chunk) {
65
+ const key = keyOf(owner, toolCallId);
66
+ const slot = entries.get(key) ?? { rows: [], carry: '' };
67
+ // Delete first so an append moves the key to the END of the ring rather
68
+ // than leaving it at its original age: the cap must evict the least
69
+ // recently WRITTEN call, not the one that started first.
70
+ entries.delete(key);
71
+ entries.set(key, slot);
72
+ const { rows, carry } = sanitizeChunk(slot.carry, chunk);
73
+ if (rows.length > 0) {
74
+ slot.rows.push(...rows);
75
+ if (slot.rows.length > LIVE_TAIL_ROWS) {
76
+ slot.rows.splice(0, slot.rows.length - LIVE_TAIL_ROWS);
77
+ }
78
+ }
79
+ slot.carry = carry;
80
+ while (entries.size > limit) {
81
+ const oldest = entries.keys().next();
82
+ if (oldest.done)
83
+ break;
84
+ entries.delete(oldest.value);
85
+ }
86
+ return tailOf(slot);
87
+ },
88
+ peek(owner, toolCallId) {
89
+ const slot = entries.get(keyOf(owner, toolCallId));
90
+ if (!slot)
91
+ return undefined;
92
+ const tail = tailOf(slot);
93
+ return tail.length > 0 ? tail : EMPTY;
94
+ },
95
+ clear(owner, toolCallId) {
96
+ entries.delete(keyOf(owner, toolCallId));
97
+ },
98
+ size() {
99
+ return entries.size;
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The live working line (agent-activity-presentation §3.2.2).
3
+ *
4
+ * Exactly one row, immediately above the composer, while a run is in flight:
5
+ *
6
+ * * Percolating...
7
+ *
8
+ * THE ROW CARRIES THE PHRASE AND NOTHING ELSE, AND THAT IS THE WHOLE OF ITS
9
+ * DESIGN (D-5 / P1-1). No elapsed clock, no token count, no `esc abort`, at any
10
+ * width. Every one of those is already on screen while running: the status bar
11
+ * renders `formatDuration(elapsedMs)` under exactly the `running` condition that
12
+ * mounts this row and the token cluster at `cols >= 72`, and both composers carry
13
+ * the abort hint. Duplicated rows are precisely what the v0.4.0 removal of the
14
+ * status bar's hint cluster was for, and that rule does not distinguish between a
15
+ * duplicated string and a duplicated number.
16
+ *
17
+ * Because there are no clusters there is NO WIDTH LADDER: one row, one
18
+ * truncating `<Text>`, identical at 200 columns and at 40. `cols` is therefore
19
+ * not a prop — nothing on the row depends on width, and a prop that changed on
20
+ * every resize but was never read would be a false dependency on a memo boundary.
21
+ *
22
+ * What is left is the one thing the status bar cannot say: THAT THE SILENCE IS
23
+ * WORK. The bar shows state; this row shows life.
24
+ */
25
+ import React from 'react';
26
+ import type { Theme } from './theme.js';
27
+ import type { TermCapabilities } from './capabilities.js';
28
+ export interface ActivityLineProps {
29
+ /** Epoch ms the current run began — the phrase sequence's seed. */
30
+ startedAt: number;
31
+ /** `Date.now() - startedAt`, already computed by App's 200 ms ticker. */
32
+ elapsedMs: number;
33
+ reducedMotion: boolean;
34
+ /**
35
+ * The name of the tool currently in flight, or `undefined`
36
+ * (agent-activity-presentation-live L4 / D-31).
37
+ *
38
+ * `status === 'running'` covers the WHOLE turn, including the minutes the
39
+ * model is idle and a child process is doing the work. Round 1 chose the
40
+ * thinking vocabulary deliberately for the gap before the first token, and it
41
+ * is right for that gap; a UI that says "Pondering" while `npm test` runs is
42
+ * not calm, it is wrong.
43
+ */
44
+ runningTool?: string;
45
+ theme: Theme;
46
+ caps: TermCapabilities;
47
+ }
48
+ export declare function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool, theme, caps, }: ActivityLineProps): React.ReactElement;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { pickGlyphs } from './glyphs.js';
5
+ import { pickActivityPhrase } from './activity-phrases.js';
6
+ export function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool, theme, caps, }) {
7
+ const glyphs = pickGlyphs(caps);
8
+ // PURE AND DERIVED FROM `(startedAt, now)`, so the sequence does not need to
9
+ // keep rotating underneath the tool label: when the tool settles the phrase
10
+ // resumes at the position the clock implies, with no state anywhere.
11
+ const phrase = pickActivityPhrase(startedAt, startedAt + elapsedMs, !reducedMotion);
12
+ // `Running ${name}` rather than a per-tool verb table: a table would be a
13
+ // SECOND registry of tool names, and `toolGlyph` is already the one place that
14
+ // knows them. Everything round 1 legislated for this row survives — one row,
15
+ // no digits, no `esc abort`, no width ladder, ASCII only.
16
+ const label = runningTool ? `Running ${runningTool}` : `${phrase}${glyphs.ellipsis}`;
17
+ // The IDENTICAL branch `Transcript.tsx` uses for the assistant role marker, and
18
+ // it must stay identical: braille dots are both an animation AND a Unicode-only
19
+ // glyph, so reduced motion and an ASCII terminal fall back to the same marker.
20
+ const spinner = caps.unicode && !reducedMotion ? _jsx(Spinner, { type: "dots" }) : glyphs.spinnerStill;
21
+ return (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [' ', spinner, " ", label] }) }));
22
+ }
package/dist/ui/App.d.ts CHANGED
@@ -8,8 +8,8 @@ import React from 'react';
8
8
  import type { AgentController } from '../agent/controller.js';
9
9
  import type { ConfirmRequest } from '../tools/index.js';
10
10
  import { type Overlay, type ToastLevel } from '../agent/reducer.js';
11
+ import type { UpdateBridge } from '../update/types.js';
11
12
  import { type RenderMode } from './layout/frame.js';
12
- import { type FrameGeometry } from './layout/regions.js';
13
13
  import type { MouseSource } from '../input/stdin-mouse-filter.js';
14
14
  import type { HumanInputBridge } from '../tools/human-input.js';
15
15
  export interface ConfirmBridge {
@@ -50,7 +50,14 @@ export interface AppProps {
50
50
  * is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
51
51
  * build (mouse-wheel-region-routing §4.2). */
52
52
  mouseSource?: MouseSource;
53
- /** Test-only seam (§8.2): frame geometry, bypassing `measureElement`. */
54
- geometryOverride?: FrameGeometry;
53
+ /**
54
+ * The auto-updater's late-arrival channel (cli-auto-update §3.8).
55
+ *
56
+ * A BRIDGE AND NOT THE SERVICE ITSELF, for the reason `confirmBridge` above is
57
+ * one: the service is built by a DYNAMIC `import()` fired after `render()`, so
58
+ * it does not exist when this component mounts. Absent under `update.mode:
59
+ * 'off'`, on a non-TTY, in CI, and in every test that does not exercise it.
60
+ */
61
+ updateBridge?: UpdateBridge;
55
62
  }
56
- export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }: AppProps): React.ReactElement;
63
+ export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }: AppProps): React.ReactElement;