@aragon-agent/cli 0.6.11 → 0.6.13

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 (115) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +97 -20
  3. package/dist/agent/controller.d.ts +38 -3
  4. package/dist/agent/controller.js +151 -60
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +199 -0
  7. package/dist/agent/queued-messages.d.ts +20 -0
  8. package/dist/agent/queued-messages.js +46 -0
  9. package/dist/agent/reducer.d.ts +21 -2
  10. package/dist/agent/reducer.js +65 -37
  11. package/dist/cli.js +15 -1
  12. package/dist/commands/builtins.js +83 -42
  13. package/dist/commands/queue-command.d.ts +3 -0
  14. package/dist/commands/queue-command.js +5 -0
  15. package/dist/commands/registry.d.ts +3 -0
  16. package/dist/compaction/child.d.ts +4 -2
  17. package/dist/compaction/child.js +4 -1
  18. package/dist/compaction/compactor.d.ts +7 -2
  19. package/dist/compaction/compactor.js +13 -11
  20. package/dist/compaction/context-command.js +5 -0
  21. package/dist/compaction/wiring.d.ts +4 -3
  22. package/dist/compaction/wiring.js +6 -1
  23. package/dist/config/cli-commands.d.ts +2 -0
  24. package/dist/config/cli-commands.js +16 -4
  25. package/dist/config/load.d.ts +2 -1
  26. package/dist/config/load.js +15 -8
  27. package/dist/config/model-profile-resolution.d.ts +20 -0
  28. package/dist/config/model-profile-resolution.js +81 -0
  29. package/dist/config/model-profile-store.d.ts +67 -0
  30. package/dist/config/model-profile-store.js +153 -0
  31. package/dist/config/model-profiles.d.ts +44 -0
  32. package/dist/config/model-profiles.js +121 -0
  33. package/dist/config/schema.d.ts +9 -0
  34. package/dist/config/store.d.ts +11 -1
  35. package/dist/config/store.js +43 -27
  36. package/dist/fast/resolve.d.ts +2 -1
  37. package/dist/fast/resolve.js +6 -4
  38. package/dist/fast/wiring.d.ts +5 -3
  39. package/dist/fast/wiring.js +8 -4
  40. package/dist/input/enter-sequences.d.ts +11 -10
  41. package/dist/input/enter-sequences.js +29 -27
  42. package/dist/input/stdin-filter.js +93 -28
  43. package/dist/logging/install.d.ts +3 -0
  44. package/dist/logging/install.js +4 -2
  45. package/dist/logging/secret-registry.d.ts +2 -0
  46. package/dist/logging/secret-registry.js +18 -0
  47. package/dist/session/persist.d.ts +6 -7
  48. package/dist/session/persist.js +6 -9
  49. package/dist/session/store.d.ts +4 -1
  50. package/dist/session/validate-session.d.ts +3 -0
  51. package/dist/session/validate-session.js +207 -0
  52. package/dist/team/runtime.d.ts +2 -1
  53. package/dist/team/subagent.d.ts +3 -1
  54. package/dist/team/subagent.js +3 -1
  55. package/dist/ui/ActivityLine.d.ts +6 -70
  56. package/dist/ui/ActivityLine.js +12 -34
  57. package/dist/ui/App.d.ts +1 -0
  58. package/dist/ui/App.js +161 -226
  59. package/dist/ui/BottomStatusRow.d.ts +6 -86
  60. package/dist/ui/BottomStatusRow.js +11 -26
  61. package/dist/ui/Composer.d.ts +18 -84
  62. package/dist/ui/Composer.js +9 -100
  63. package/dist/ui/PromptInput.d.ts +6 -2
  64. package/dist/ui/PromptInput.js +91 -105
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +24 -0
  68. package/dist/ui/QueueStatusRow.js +51 -0
  69. package/dist/ui/StatusBar.d.ts +15 -149
  70. package/dist/ui/StatusBar.js +27 -112
  71. package/dist/ui/clipboard-task.d.ts +29 -0
  72. package/dist/ui/clipboard-task.js +66 -0
  73. package/dist/ui/clipboard.d.ts +29 -50
  74. package/dist/ui/clipboard.js +141 -75
  75. package/dist/ui/composer-input.d.ts +28 -0
  76. package/dist/ui/composer-input.js +46 -0
  77. package/dist/ui/composer-limits.d.ts +3 -4
  78. package/dist/ui/composer-limits.js +4 -9
  79. package/dist/ui/composer-rows.d.ts +1 -1
  80. package/dist/ui/composer-rows.js +24 -137
  81. package/dist/ui/editor-navigation.d.ts +45 -0
  82. package/dist/ui/editor-navigation.js +85 -0
  83. package/dist/ui/editor-reducer.d.ts +9 -0
  84. package/dist/ui/editor-reducer.js +31 -15
  85. package/dist/ui/enter-frames.d.ts +15 -31
  86. package/dist/ui/enter-frames.js +14 -33
  87. package/dist/ui/interaction-copy.d.ts +77 -0
  88. package/dist/ui/interaction-copy.js +40 -0
  89. package/dist/ui/interaction-hints.d.ts +18 -0
  90. package/dist/ui/interaction-hints.js +92 -0
  91. package/dist/ui/layout/budget.js +1 -1
  92. package/dist/ui/layout/queue-layout.d.ts +19 -0
  93. package/dist/ui/layout/queue-layout.js +26 -0
  94. package/dist/ui/layout/status-layout.d.ts +55 -0
  95. package/dist/ui/layout/status-layout.js +109 -0
  96. package/dist/ui/layout/todo-layout.d.ts +1 -0
  97. package/dist/ui/layout/todo-layout.js +1 -1
  98. package/dist/ui/overlays/HelpOverlay.js +11 -3
  99. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  100. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  101. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  102. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  103. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  104. package/dist/ui/overlays/QueueOverlay.js +79 -0
  105. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  106. package/dist/ui/overlays/SettingsScreen.js +347 -3
  107. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  108. package/dist/ui/overlays/model-profile-state.js +119 -0
  109. package/dist/ui/run-status-row.d.ts +7 -19
  110. package/dist/ui/run-status-row.js +2 -18
  111. package/dist/ui/selection/selection-controller.d.ts +2 -2
  112. package/dist/ui/selection/selection-controller.js +58 -20
  113. package/dist/ui/transcript-text.js +20 -12
  114. package/dist/ui/use-wheel-routing.js +2 -1
  115. package/package.json +2 -2
@@ -0,0 +1,207 @@
1
+ import { isAdapterProvider } from '../config/schema.js';
2
+ import { normalizeTodos } from '../todo/normalize.js';
3
+ import { TODO_LIMITS } from '../todo/limits.js';
4
+ function fail(path, expected) {
5
+ throw new Error(`Invalid session file: ${path} must be ${expected}.`);
6
+ }
7
+ function record(value, path) {
8
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
9
+ fail(path, 'an object');
10
+ }
11
+ return value;
12
+ }
13
+ const string = (value, path) => {
14
+ if (typeof value !== 'string')
15
+ fail(path, 'a string');
16
+ };
17
+ const nonempty = (value, path) => {
18
+ if (typeof value !== 'string' || value.trim().length === 0)
19
+ fail(path, 'a non-empty string');
20
+ };
21
+ const number = (value, path) => {
22
+ if (typeof value !== 'number' || !Number.isFinite(value))
23
+ fail(path, 'a finite number');
24
+ };
25
+ const boolean = (value, path) => {
26
+ if (typeof value !== 'boolean')
27
+ fail(path, 'a boolean');
28
+ };
29
+ const object = (value, path) => { record(value, path); };
30
+ function optional(check) {
31
+ return (value, path) => { if (value !== undefined)
32
+ check(value, path); };
33
+ }
34
+ function enumeration(...values) {
35
+ return (value, path) => {
36
+ if (typeof value !== 'string' || !values.includes(value))
37
+ fail(path, values.join(' | '));
38
+ };
39
+ }
40
+ function array(check) {
41
+ return (value, path) => {
42
+ if (!Array.isArray(value))
43
+ fail(path, 'an array');
44
+ value.forEach((item, index) => check(item, `${path}[${index}]`));
45
+ };
46
+ }
47
+ function shape(fields) {
48
+ return (value, path) => {
49
+ const source = record(value, path);
50
+ for (const [key, check] of Object.entries(fields))
51
+ check(source[key], `${path}.${key}`);
52
+ };
53
+ }
54
+ const usage = shape({
55
+ inputTokens: number, outputTokens: number, cacheReadTokens: optional(number),
56
+ cacheWriteTokens: optional(number), cost: optional(number),
57
+ });
58
+ function contentPart(value, path) {
59
+ const part = record(value, path);
60
+ enumeration('text', 'image')(part.type, `${path}.type`);
61
+ if (part.type === 'text')
62
+ string(part.text, `${path}.text`);
63
+ else
64
+ shape({ mediaType: string, data: string })(part, path);
65
+ }
66
+ function contentBlock(value, path) {
67
+ const block = record(value, path);
68
+ enumeration('text', 'thinking', 'tool_call')(block.type, `${path}.type`);
69
+ if (block.type === 'tool_call') {
70
+ shape({ toolCallId: string, toolName: string, args: object })(block, path);
71
+ return;
72
+ }
73
+ string(block.text, `${path}.text`);
74
+ if (block.type === 'thinking')
75
+ optional(string)(block.signature, `${path}.signature`);
76
+ }
77
+ function message(value, path) {
78
+ const source = record(value, path);
79
+ enumeration('user', 'assistant', 'tool_result')(source.role, `${path}.role`);
80
+ if (source.role === 'assistant') {
81
+ shape({ content: array(contentBlock), usage: optional(usage),
82
+ stopReason: optional(enumeration('end_turn', 'tool_use', 'max_tokens', 'stop_sequence')),
83
+ })(source, path);
84
+ return;
85
+ }
86
+ if (source.role === 'tool_result') {
87
+ shape({ toolCallId: string, isError: optional(boolean) })(source, path);
88
+ }
89
+ else
90
+ optional(number)(source.timestamp, `${path}.timestamp`);
91
+ if (typeof source.content !== 'string')
92
+ array(contentPart)(source.content, `${path}.content`);
93
+ }
94
+ const patchLine = shape({
95
+ kind: enumeration('ctx', 'add', 'del'), text: string,
96
+ oldLine: optional(number), newLine: optional(number),
97
+ });
98
+ const patch = shape({
99
+ path: string, kind: enumeration('create', 'update'), added: number, removed: number,
100
+ truncated: boolean, lineCount: number,
101
+ degraded: optional(enumeration('binary', 'too-large', 'unreadable')),
102
+ hunks: array(shape({ oldStart: number, oldCount: number, newStart: number, newCount: number,
103
+ lines: array(patchLine) })),
104
+ });
105
+ const todo = shape({
106
+ content: string, activeForm: string, status: enumeration('pending', 'in_progress', 'completed'),
107
+ });
108
+ const subagent = shape({
109
+ label: string, description: string,
110
+ // Old team archives predate tier; renderers already treat absence as main.
111
+ tier: optional(enumeration('main', 'fast')),
112
+ phase: enumeration('queued', 'starting', 'thinking', 'tool', 'waiting', 'done', 'failed', 'aborted'),
113
+ startedAt: optional(number), endedAt: optional(number), turns: number, toolCalls: number,
114
+ lastTool: optional(string), usage, filesTouched: array(string), messagesSent: number,
115
+ summary: optional(string), error: optional(string), truncated: optional(boolean),
116
+ compactions: optional(number), activity: optional(string),
117
+ activityArgs: optional(shape({ path: optional(string), pattern: optional(string),
118
+ command: optional(string), to: optional(string), from: optional(string) })),
119
+ blockedWaits: optional(number), retries: optional(number), retryable: optional(boolean),
120
+ retry: optional(shape({ attempt: number, maxRetries: number })),
121
+ });
122
+ const ENTRY_FIELDS = {
123
+ user: { text: string },
124
+ queued: { text: string, queueId: optional(nonempty) },
125
+ notice: { text: string, level: enumeration('info', 'warn', 'error') },
126
+ assistant: {
127
+ text: string, thinking: optional(string), thinkingOpen: boolean, streaming: boolean,
128
+ aborted: optional(boolean), usage: optional(usage),
129
+ thinkingStartedAt: optional(number), thinkingMs: optional(number),
130
+ },
131
+ tool: {
132
+ toolCallId: string, name: string, label: string, argsRaw: string, args: optional(object),
133
+ status: enumeration('pending', 'running', 'done', 'error'), preview: optional(string),
134
+ durationMs: optional(number), isError: optional(boolean), patch: optional(patch),
135
+ live: optional(array(string)), liveSeq: optional(number), lastOutputAt: optional(number),
136
+ },
137
+ team: { dispatchId: string, requested: number, runs: array(subagent), aborted: boolean,
138
+ durationMs: optional(number), active: boolean },
139
+ todo: { items: array(todo), doneCount: number, total: number, live: boolean,
140
+ interrupted: optional(boolean) },
141
+ retry: {
142
+ attempt: number, maxRetries: number, errorType: string, message: string, delayMs: number,
143
+ resumeAt: optional(number), startedAt: number, totalRetries: optional(number),
144
+ elapsedMs: optional(number),
145
+ phase: enumeration('waiting', 'retrying', 'recovered', 'exhausted', 'interrupted'),
146
+ },
147
+ fast: { reviewIndex: number, model: string,
148
+ status: enumeration('running', 'ok', 'advice', 'empty', 'failed', 'dropped'),
149
+ text: optional(string), detail: optional(string), turn: number,
150
+ durationMs: optional(number), live: boolean },
151
+ compaction: {
152
+ index: number, trigger: enumeration('pressure', 'overflow', 'manual'),
153
+ mode: enumeration('summarized', 'truncated', 'relieved', 'none'), applied: boolean,
154
+ reason: optional(string), messagesBefore: number, messagesAfter: number,
155
+ tokensBefore: number, tokensAfter: number, summary: optional(string), model: string,
156
+ durationMs: optional(number), startedAt: optional(number), live: boolean,
157
+ tailRelief: optional(shape({ messages: number, charsRemoved: number })),
158
+ },
159
+ service: {
160
+ serviceId: string, command: string,
161
+ status: enumeration('starting', 'ready', 'running', 'exited', 'failed', 'stopped'),
162
+ url: optional(string), port: optional(number),
163
+ exitCode: (value, path) => { if (value !== null)
164
+ number(value, path); },
165
+ startedAt: number, readyAt: optional(number), endedAt: optional(number), rows: array(string),
166
+ rowsSeen: number, terminal: optional(boolean), killIncomplete: optional(boolean),
167
+ },
168
+ };
169
+ function validateEntries(value) {
170
+ const ids = new Set();
171
+ const queues = new Set();
172
+ array((item, path) => {
173
+ const entry = record(item, path);
174
+ nonempty(entry.id, `${path}.id`);
175
+ if (ids.has(entry.id))
176
+ fail(`${path}.id`, 'unique');
177
+ ids.add(entry.id);
178
+ enumeration(...Object.keys(ENTRY_FIELDS))(entry.kind, `${path}.kind`);
179
+ shape(ENTRY_FIELDS[entry.kind])(entry, path);
180
+ if (entry.kind !== 'queued' || entry.queueId === undefined)
181
+ return;
182
+ if (queues.has(entry.queueId))
183
+ fail(`${path}.queueId`, 'unique');
184
+ queues.add(entry.queueId);
185
+ })(value, 'entries');
186
+ }
187
+ function validateModel(value) {
188
+ const model = record(value, 'model');
189
+ shape({ providerId: nonempty, modelId: nonempty, baseUrl: optional(string) })(model, 'model');
190
+ if (typeof model.providerId !== 'string' || !isAdapterProvider(model.providerId)) {
191
+ fail('model.providerId', 'a supported adapter');
192
+ }
193
+ }
194
+ /** Return a prepared copy, preserving extensions; throw with the corrupt field path. */
195
+ export function validateSession(value) {
196
+ const source = record(value, 'session');
197
+ array(message)(source.messages, 'messages');
198
+ validateEntries(source.entries);
199
+ if (source.model !== undefined)
200
+ validateModel(source.model);
201
+ // Missing fields remain missing in old files, including exec metadata.
202
+ const prepared = { ...source };
203
+ if (source.todos !== undefined) {
204
+ prepared.todos = normalizeTodos(source.todos, TODO_LIMITS.maxItems).items;
205
+ }
206
+ return prepared;
207
+ }
@@ -17,6 +17,7 @@
17
17
  * queue class, no event-loop trickery — every path is observable from the event
18
18
  * stream.
19
19
  */
20
+ import type { ModelRole } from '../config/model-profiles.js';
20
21
  import type { ProviderRegistry, SkillRegistry, ToolPolicyDecision, ToolPolicyVerdict } from '@aragon-agent/core';
21
22
  import type { AgentMode } from '../agent/agent-mode.js';
22
23
  import type { CliConfig } from '../config/schema.js';
@@ -30,7 +31,7 @@ export interface TeamRuntimeDeps {
30
31
  providerRegistry: ProviderRegistry;
31
32
  getCwd: () => string;
32
33
  getMode: () => AgentMode;
33
- getApiKey: (providerId: string) => string | undefined;
34
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
34
35
  skillRegistry?: SkillRegistry;
35
36
  alwaysBlock?: () => string;
36
37
  toolPolicy?: () => ToolPolicyDecision;
@@ -10,6 +10,7 @@
10
10
  * them (§3.11 / R-6). What a child does NOT get is spelled out below, and each
11
11
  * omission has a reason that is a correctness boundary rather than caution.
12
12
  */
13
+ import type { ModelRole } from '../config/model-profiles.js';
13
14
  import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ModelRef, type ProviderRegistry, type SkillRegistry, type ThinkingLevel, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
14
15
  import type { AgentMode } from '../agent/agent-mode.js';
15
16
  import type { ChildContextManagerProvider } from '../compaction/child.js';
@@ -60,7 +61,7 @@ export interface SubagentDeps {
60
61
  getCwd: () => string;
61
62
  /** The lead's live session mode. Read at CALL time, never cached. */
62
63
  getMode: () => AgentMode;
63
- getApiKey: (providerId: string) => string | undefined;
64
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
64
65
  /**
65
66
  * The lead's `SkillRegistry`, used for `skill_find` ONLY.
66
67
  *
@@ -109,6 +110,7 @@ export interface SubagentDeps {
109
110
  resolveTier?: (tier: FastTierName) => {
110
111
  ref: ModelRef;
111
112
  thinkingLevel: ThinkingLevel;
113
+ role?: ModelRole;
112
114
  };
113
115
  /**
114
116
  * Build this child its own context manager
@@ -181,6 +181,7 @@ export function createSubagent(spec, deps, hooks) {
181
181
  ...(deps.config.baseUrl ? { baseUrl: deps.config.baseUrl } : {}),
182
182
  },
183
183
  thinkingLevel: deps.config.thinkingLevel,
184
+ role: 'main',
184
185
  };
185
186
  const model = resolved.ref;
186
187
  // HOISTED TO A LOCAL, AND THAT IS WHAT MAKES W3 POSSIBLE AT ALL. This was built
@@ -209,6 +210,7 @@ export function createSubagent(spec, deps, hooks) {
209
210
  const contextManager = deps.contextManagerFor?.({
210
211
  label: spec.label,
211
212
  model,
213
+ role: resolved.role ?? 'main',
212
214
  getMessages: () => agentRef?.state.messages ?? [],
213
215
  getSystemPrompt: () => systemPrompt,
214
216
  onCompacted: () => {
@@ -224,7 +226,7 @@ export function createSubagent(spec, deps, hooks) {
224
226
  // constructing five of them per dispatch is pure waste.
225
227
  providerRegistry: deps.providerRegistry,
226
228
  // The lead's closure, so a settings-screen key edit reaches children too.
227
- getApiKey: deps.getApiKey,
229
+ getApiKey: (id) => deps.getApiKey(id, resolved.role ?? 'main'),
228
230
  ...(deps.config.maxTokens !== undefined ? { maxTokens: deps.config.maxTokens } : {}),
229
231
  timeouts: {
230
232
  toolTimeout: deps.config.toolTimeoutMs,
@@ -1,85 +1,21 @@
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
- * THIS ROW OWNS THE ONLY ANIMATED SPINNER WHILE IT IS MOUNTED
26
- * (single-spinner-while-running D-1). Seven other sites can animate — the
27
- * streaming assistant marker, the tool badge, the team/fast/retry cards, and the
28
- * two panels' rows — and each was independently right to. Emergently they put up
29
- * to five braille animations on adjacent rows, each on its own 80 ms timer: one
30
- * piece of information rendered five times and out of phase, which is the same
31
- * duplication the paragraph above legislates against for text. So `App` widens
32
- * the `reducedMotion` it hands those seven to `cfg.reducedMotion || (running &&
33
- * !overlay)`, and they all fall back to the static glyphs they already had.
34
- *
35
- * THIS COMPONENT KEEPS THE RAW CONFIG FLAG, and that asymmetry is deliberate
36
- * (D-5). It reads the flag TWICE: once for the spinner below, and once for
37
- * `pickActivityPhrase`'s rotation. Handing it the widened value would freeze the
38
- * phrase at the first word of every run — a second, unrelated regression, and
39
- * one nobody would connect to a change about icons.
1
+ /** Legacy activity presentation. Explicit facts share the global status label mapping.
2
+ * StatusBar owns the application's sole live spinner via the factory below.
40
3
  */
41
4
  import React from 'react';
42
5
  import type { Theme } from './theme.js';
43
6
  import type { TermCapabilities } from './capabilities.js';
44
7
  import { pickGlyphs } from './glyphs.js';
45
8
  import type { RunActivity } from './run-status-row.js';
46
- /**
47
- * The animated spinner this row draws while it is live, or `null` when it would
48
- * be STATIC — an ASCII terminal, or a user who asked for stillness.
49
- *
50
- * A FACTORY, NOT A COMPONENT, and the distinction is load-bearing. A
51
- * `<LiveSpinner/>` element whose own render returned `null` would still be
52
- * truthy at the call site — the exact trap `BottomStatusRow`'s `update` prop
53
- * documents at length. Callers test the RETURN VALUE and get a real answer.
54
- *
55
- * It is exported so `BottomStatusRow` can put the bare glyph beside a toast
56
- * without importing `ink-spinner` itself: `spinner-census.test.ts` enumerates
57
- * every file under `src/ui/**` that imports it, and that list is the checklist
58
- * for "who is allowed to animate". One more importer is one more site a future
59
- * reader has to reason about; one more caller of this function is not.
60
- */
9
+ /** Shared animation factory; returns null for ASCII or reduced-motion terminals. */
61
10
  export declare function liveSpinner(reducedMotion: boolean, caps: TermCapabilities): React.ReactElement | null;
62
- /**
63
- * The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
64
- * because compaction can be the only thing happening, and the tool name outranks
65
- * the rotating phrase for the reason `runningTool` exists at all. One
66
- * implementation, because the fixed bottom row and the run status row above the
67
- * input must never disagree about what the agent is doing.
68
- *
69
- * `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
70
- * registry of tool names, and `toolGlyph` is already the one place that knows
71
- * them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
72
- */
11
+ /** Explicit execution facts outrank compatibility tool names and rotating phrases. */
73
12
  export declare function resolveActivityLabel(activity: RunActivity, glyphs: ReturnType<typeof pickGlyphs>): string;
74
13
  export interface ActivityLabelProps extends RunActivity {
75
14
  /**
76
- * False draws the STATIC glyph even when motion is allowed. The run status row
77
- * uses it when it has scrolled out of the viewport (the fixed bottom row owns
78
- * the animation then) or an overlay covers the chat. The phrase still rotates
79
- * from the RAW `reducedMotion`, so a still icon never freezes the wording.
15
+ * False forces a static glyph. Compatibility phrases still follow reducedMotion.
80
16
  */
81
17
  spinnerLive?: boolean;
82
- /** Prefix one space (the fixed bottom row's indent); the run row indents itself. */
18
+ /** Prefix one space for compatibility line consumers. */
83
19
  leadingSpace?: boolean;
84
20
  theme: Theme;
85
21
  caps: TermCapabilities;
@@ -3,36 +3,19 @@ import { Box, Text } from 'ink';
3
3
  import Spinner from 'ink-spinner';
4
4
  import { pickGlyphs } from './glyphs.js';
5
5
  import { pickActivityPhrase } from './activity-phrases.js';
6
- /**
7
- * The animated spinner this row draws while it is live, or `null` when it would
8
- * be STATIC — an ASCII terminal, or a user who asked for stillness.
9
- *
10
- * A FACTORY, NOT A COMPONENT, and the distinction is load-bearing. A
11
- * `<LiveSpinner/>` element whose own render returned `null` would still be
12
- * truthy at the call site — the exact trap `BottomStatusRow`'s `update` prop
13
- * documents at length. Callers test the RETURN VALUE and get a real answer.
14
- *
15
- * It is exported so `BottomStatusRow` can put the bare glyph beside a toast
16
- * without importing `ink-spinner` itself: `spinner-census.test.ts` enumerates
17
- * every file under `src/ui/**` that imports it, and that list is the checklist
18
- * for "who is allowed to animate". One more importer is one more site a future
19
- * reader has to reason about; one more caller of this function is not.
20
- */
6
+ import { phaseLabels } from './layout/status-layout.js';
7
+ /** Shared animation factory; returns null for ASCII or reduced-motion terminals. */
21
8
  export function liveSpinner(reducedMotion, caps) {
22
9
  return caps.unicode && !reducedMotion ? _jsx(Spinner, { type: "dots" }) : null;
23
10
  }
24
- /**
25
- * The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
26
- * because compaction can be the only thing happening, and the tool name outranks
27
- * the rotating phrase for the reason `runningTool` exists at all. One
28
- * implementation, because the fixed bottom row and the run status row above the
29
- * input must never disagree about what the agent is doing.
30
- *
31
- * `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
32
- * registry of tool names, and `toolGlyph` is already the one place that knows
33
- * them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
34
- */
11
+ /** Explicit execution facts outrank compatibility tool names and rotating phrases. */
35
12
  export function resolveActivityLabel(activity, glyphs) {
13
+ if (activity.phase !== undefined || activity.activeTool !== undefined || activity.outcome !== undefined) {
14
+ return phaseLabels({
15
+ phase: activity.phase ?? (activity.activeTool ? 'tool' : 'idle'),
16
+ activeTool: activity.activeTool, runOutcome: activity.outcome, compacting: activity.compacting,
17
+ })[1];
18
+ }
36
19
  if (activity.compacting)
37
20
  return `Compacting context${glyphs.ellipsis}`;
38
21
  if (activity.runningTool)
@@ -47,14 +30,9 @@ export function resolveActivityLabel(activity, glyphs) {
47
30
  export function ActivityLabel({ spinnerLive = true, leadingSpace = false, theme, caps, ...activity }) {
48
31
  const glyphs = pickGlyphs(caps);
49
32
  const label = resolveActivityLabel(activity, glyphs);
50
- // The IDENTICAL branch `Transcript.tsx` uses for the assistant role marker, and
51
- // it must stay identical: braille dots are both an animation AND a Unicode-only
52
- // glyph, so reduced motion and an ASCII terminal fall back to the same marker.
53
- //
54
- // Through `liveSpinner` rather than inline, so this row and the glyph
55
- // `BottomStatusRow` composes beside a toast can never disagree about WHEN the
56
- // spinner is live - the same reason `App` names its mount condition once.
57
- const spinner = (spinnerLive ? liveSpinner(activity.reducedMotion, caps) : null) ??
33
+ const settled = activity.phase === 'idle' || (activity.phase === undefined
34
+ && activity.outcome !== undefined && activity.outcome !== 'none' && !activity.activeTool);
35
+ const spinner = (spinnerLive && (!settled || activity.compacting) ? liveSpinner(activity.reducedMotion, caps) : null) ??
58
36
  glyphs.spinnerStill;
59
37
  return (_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [leadingSpace ? ' ' : '', spinner, " ", label] }));
60
38
  }
package/dist/ui/App.d.ts CHANGED
@@ -88,6 +88,7 @@ export interface TerminalBridge {
88
88
  scrollbar?: ScrollbarBridge;
89
89
  /** Whether drag-select is on for this session (`mouseSelect` + a filter). */
90
90
  mouseSelect: boolean;
91
+ deleteDisambiguated?: boolean;
91
92
  /** Release the mouse to the terminal, or take it back (`/mouse`, G3). */
92
93
  setMouseCapture?: (on: boolean) => void;
93
94
  /** Whether the mouse is captured right now — read at command time. */