@abelxiaoxing/cadence 1.8.4 → 2.0.1

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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +92 -20
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
@@ -1,1417 +0,0 @@
1
- import type {
2
- ExtensionUIContext,
3
- ToolRenderResultOptions,
4
- } from "@earendil-works/pi-coding-agent";
5
- import type { Component } from "@earendil-works/pi-tui";
6
- import {
7
- stripTerminalSequences,
8
- Text,
9
- truncateToWidth,
10
- visibleWidth,
11
- } from "@earendil-works/pi-tui";
12
- import type {
13
- PacketActivityEvent,
14
- PacketActivityObserver,
15
- } from "./activity-contracts.ts";
16
- import {
17
- projectDesignDiagnostic,
18
- type SafeDesignDiagnostic,
19
- } from "./design-diagnostics.ts";
20
-
21
- export const ACTIVITY_DETAILS_KEY = "activityDisplay" as const;
22
- export const ACTIVITY_WIDGET_KEY = "abel-subagents" as const;
23
- export const ACTIVITY_STATUS_KEY = "abel-subagents" as const;
24
- export const ACTIVITY_REFRESH_MS = 100;
25
- export const ACTIVITY_WIDGET_MAX_LINES = 12;
26
- export const ACTIVITY_WIDGET_MIN_WIDTH = 3;
27
-
28
- export type {
29
- ActivityState,
30
- TerminalActivityState,
31
- WorkflowActivityState,
32
- WorkflowActivityUpdate,
33
- } from "./activity-contracts.ts";
34
- export { WORKFLOW_ACTIVITY_STATES } from "./activity-contracts.ts";
35
-
36
- import type {
37
- ActivityState,
38
- TerminalActivityState,
39
- WorkflowActivityState,
40
- WorkflowActivityUpdate,
41
- } from "./activity-contracts.ts";
42
-
43
- export interface EvidenceActivitySummary {
44
- kind: "evidence";
45
- conclusions: number;
46
- citations: number;
47
- risks: number;
48
- blockingQuestions: number;
49
- }
50
-
51
- export interface DiffActivitySummary {
52
- kind: "diff";
53
- summary: string;
54
- riskCount: number;
55
- retained: boolean;
56
- }
57
-
58
- export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
59
- export type ActivityTone = "accent" | "success" | "warning" | "muted" | "error";
60
-
61
- export interface ActivityDisplay {
62
- kind: "activityDisplay";
63
- requestId: string;
64
- role: string;
65
- phase: string;
66
- objective: string;
67
- state: TerminalActivityState;
68
- tone: ActivityTone;
69
- elapsedMs: number;
70
- summary?: ActivitySummary;
71
- reason?: string;
72
- runId?: string;
73
- taskId?: string;
74
- code?: string;
75
- nextAction?: string;
76
- attempt?: number;
77
- maxAttempts?: number;
78
- wait?: string;
79
- }
80
-
81
- export interface ActivitySnapshot {
82
- toolCallId: string;
83
- requestId: string;
84
- role: string;
85
- phase: string;
86
- objective: string;
87
- state: ActivityState;
88
- sequence: number;
89
- startedAt: number;
90
- elapsedMs: number;
91
- spinnerFrame?: number;
92
- tone?: ActivityTone;
93
- runId?: string;
94
- taskId?: string;
95
- code?: string;
96
- nextAction?: string;
97
- attempt?: number;
98
- maxAttempts?: number;
99
- wait?: string;
100
- }
101
-
102
- interface ActivityEntry extends ActivitySnapshot {
103
- onUpdate?: (result: unknown) => void;
104
- }
105
-
106
- type ActivityColor =
107
- | "accent"
108
- | "success"
109
- | "error"
110
- | "warning"
111
- | "muted"
112
- | "text";
113
- type ActivityTheme = {
114
- fg?: (color: ActivityColor, text: string) => string;
115
- };
116
- type ActivityUi = Pick<ExtensionUIContext, "setWidget" | "setStatus">;
117
- type ActivityRenderContext = {
118
- args?: unknown;
119
- executionStarted?: boolean;
120
- isError?: boolean;
121
- };
122
- type TimerHandle = ReturnType<typeof setInterval>;
123
-
124
- export interface ActivityControllerOptions {
125
- now?: () => number;
126
- setInterval?: (callback: () => void, delay: number) => TimerHandle | unknown;
127
- clearInterval?: (handle: TimerHandle | unknown) => void;
128
- }
129
-
130
- function boundedWidth(width: number): number {
131
- return Math.max(0, Number.isFinite(width) ? Math.floor(width) : 0);
132
- }
133
-
134
- function normalizeWhitespace(value: string): string {
135
- return stripTerminalSequences(value).replace(/\s+/g, " ").trim();
136
- }
137
-
138
- function redactPaths(value: string): string {
139
- return value
140
- .replace(
141
- /(?:[A-Za-z]:[\\/]|\.{1,2}[\\/]|\/)(?:[^\s,;:()[\]{}]+[\\/])*[^\s,;:()[\]{}]+/g,
142
- "[path]",
143
- )
144
- .replace(/(?:\b[\w.-]+[\\/])+[\w.-]+/g, "[path]")
145
- .replace(/\b[\w.-]+\.[A-Za-z0-9]{1,12}\b/g, "[path]");
146
- }
147
-
148
- function redactProviderAndModel(value: string): string {
149
- return value.replace(
150
- /\b(?:provider|model)(?:\s+identity)?\s*(?:[:=]|\/)?\s*[^\s,;()[\]]+/gi,
151
- "[redacted]",
152
- );
153
- }
154
-
155
- export function sanitizeDisplayText(value: unknown, maxLength = 240): string {
156
- const text = typeof value === "string" ? value : String(value ?? "");
157
- const safe = redactProviderAndModel(redactPaths(normalizeWhitespace(text)));
158
- return safe.slice(0, Math.max(0, maxLength));
159
- }
160
-
161
- const SAFE_WORKFLOW_CODE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
162
- const SAFE_DESIGN_DETAIL_VALUE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
163
- const SAFE_CONTROL_ACTIONS = new Set([
164
- "start",
165
- "status",
166
- "resume",
167
- "rebind",
168
- "cancel",
169
- "discard",
170
- ]);
171
-
172
- export function sanitizeWorkflowCode(value: unknown): string {
173
- const code = typeof value === "string" ? value : "";
174
- return SAFE_WORKFLOW_CODE.test(code) ? code : "workflow-state-unavailable";
175
- }
176
-
177
- function sanitizeControlAction(value: unknown): string {
178
- const action = typeof value === "string" ? value : "";
179
- return SAFE_CONTROL_ACTIONS.has(action) ? action : "status";
180
- }
181
-
182
- const SAFE_FAILURE_REASONS = new Set([
183
- "subagent failed",
184
- "subagent cancelled",
185
- "phase timed out",
186
- ]);
187
-
188
- export function sanitizeFailureReason(value: unknown, maxLength = 240): string {
189
- const normalized = normalizeWhitespace(String(value ?? ""));
190
- const safe = SAFE_FAILURE_REASONS.has(normalized)
191
- ? normalized
192
- : "subagent failed";
193
- return safe.slice(0, Math.max(0, maxLength));
194
- }
195
-
196
- function safeDesignFailure(value: unknown): {
197
- operation: string;
198
- code: string;
199
- diagnostics: SafeDesignDiagnostic[];
200
- } | null {
201
- const failure = asRecord(value);
202
- if (
203
- failure?.kind !== "design-control-failure" ||
204
- typeof failure.operation !== "string" ||
205
- !SAFE_DESIGN_DETAIL_VALUE.test(failure.operation) ||
206
- typeof failure.code !== "string" ||
207
- !SAFE_DESIGN_DETAIL_VALUE.test(failure.code) ||
208
- !Array.isArray(failure.diagnostics)
209
- ) {
210
- return null;
211
- }
212
- const diagnostics = failure.diagnostics.slice(0, 64).flatMap((candidate) => {
213
- const diagnostic = projectDesignDiagnostic(candidate);
214
- return diagnostic ? [diagnostic] : [];
215
- });
216
- return {
217
- operation: failure.operation,
218
- code: failure.code,
219
- diagnostics,
220
- };
221
- }
222
-
223
- function renderDesignFailure(
224
- failure: ReturnType<typeof safeDesignFailure> & object,
225
- expanded: boolean,
226
- theme?: ActivityTheme,
227
- ): Component {
228
- const title = styled(
229
- theme,
230
- "error",
231
- `${dispatchLabel("design")} ${failure.operation} failed: ${failure.code}`,
232
- );
233
- if (!expanded || failure.diagnostics.length === 0) {
234
- const suffix =
235
- failure.diagnostics.length > 0
236
- ? styled(
237
- theme,
238
- "muted",
239
- ` (${failure.diagnostics.length} diagnostic${failure.diagnostics.length === 1 ? "" : "s"})`,
240
- )
241
- : "";
242
- return new Text(`${title}${suffix}`, 0, 0);
243
- }
244
- const lines = failure.diagnostics.map((diagnostic) => {
245
- const details = Object.entries(diagnostic)
246
- .filter(([key]) => key !== "code")
247
- .map(
248
- ([key, value]) =>
249
- `${key}=${Array.isArray(value) ? JSON.stringify(value) : value}`,
250
- )
251
- .join(" · ");
252
- return ` - ${diagnostic.code}${details ? ` · ${details}` : ""}`;
253
- });
254
- return new Text(
255
- [title, ...lines.map((entry) => styled(theme, "muted", entry))].join("\n"),
256
- 0,
257
- 0,
258
- );
259
- }
260
-
261
- export function formatElapsed(milliseconds: number): string {
262
- const value = Math.max(
263
- 0,
264
- Math.floor(Number.isFinite(milliseconds) ? milliseconds : 0),
265
- );
266
- if (value < 1_000) return `${value}ms`;
267
- const seconds = Math.floor(value / 1_000);
268
- if (seconds < 60) return `${seconds}s`;
269
- const minutes = Math.floor(seconds / 60);
270
- return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
271
- }
272
-
273
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
274
-
275
- function stateGlyph(state: ActivityState, spinnerFrame = 0): string {
276
- switch (state) {
277
- case "queued":
278
- return "…";
279
- case "preparing":
280
- case "connecting":
281
- case "waiting-first-response":
282
- case "running":
283
- case "validating":
284
- case "retrying":
285
- case "verifying":
286
- case "applying":
287
- case "recovering":
288
- return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] ?? "⠋";
289
- case "completed":
290
- return "✓";
291
- case "failed":
292
- case "rejected":
293
- return "✗";
294
- case "cancelled":
295
- case "operation-cancelled":
296
- return "⊘";
297
- case "discarded":
298
- return "◇";
299
- case "paused":
300
- case "approval-needed":
301
- return "!";
302
- case "timed-out":
303
- return "⌛";
304
- }
305
- }
306
-
307
- function line(value: string, width: number): string {
308
- const bounded = truncateToWidth(value, boundedWidth(width), "");
309
- return visibleWidth(bounded) <= boundedWidth(width)
310
- ? bounded
311
- : truncateToWidth(bounded, boundedWidth(width), "");
312
- }
313
-
314
- function stateText(state: ActivityState): string {
315
- return state;
316
- }
317
-
318
- function activityMetadata(
319
- snapshot: ActivitySnapshot | ActivityDisplay,
320
- ): string[] {
321
- const metadata: string[] = [];
322
- if (snapshot.taskId) {
323
- metadata.push(`task ${sanitizeDisplayText(snapshot.taskId, 80)}`);
324
- }
325
- if (snapshot.code) {
326
- metadata.push(`code ${sanitizeWorkflowCode(snapshot.code)}`);
327
- }
328
- if (
329
- Number.isSafeInteger(snapshot.attempt) &&
330
- (snapshot.attempt as number) > 0
331
- ) {
332
- metadata.push(
333
- Number.isSafeInteger(snapshot.maxAttempts) &&
334
- (snapshot.maxAttempts as number) >= (snapshot.attempt as number)
335
- ? `attempt ${snapshot.attempt}/${snapshot.maxAttempts}`
336
- : `attempt ${snapshot.attempt}`,
337
- );
338
- }
339
- if (snapshot.wait) {
340
- metadata.push(`wait ${sanitizeDisplayText(snapshot.wait, 80)}`);
341
- }
342
- if (snapshot.nextAction) {
343
- metadata.push(`next ${sanitizeControlAction(snapshot.nextAction)}`);
344
- }
345
- return metadata;
346
- }
347
-
348
- function styled(
349
- theme: ActivityTheme | undefined,
350
- color: ActivityColor,
351
- value: string,
352
- ): string {
353
- try {
354
- return theme?.fg?.(color, value) ?? value;
355
- } catch {
356
- return value;
357
- }
358
- }
359
-
360
- function renderActivityLines(
361
- snapshot: ActivitySnapshot | ActivityDisplay,
362
- width: number,
363
- theme?: ActivityTheme,
364
- now = Date.now(),
365
- ): string[] {
366
- const elapsed =
367
- "kind" in snapshot
368
- ? snapshot.elapsedMs
369
- : Math.max(0, now - snapshot.startedAt);
370
- const first = [
371
- stateGlyph(
372
- snapshot.state,
373
- "spinnerFrame" in snapshot ? snapshot.spinnerFrame : undefined,
374
- ),
375
- "Subagent",
376
- sanitizeDisplayText(snapshot.role, 80),
377
- `#${sanitizeDisplayText(snapshot.requestId, 128)}`,
378
- sanitizeDisplayText(snapshot.phase, 40),
379
- stateText(snapshot.state),
380
- formatElapsed(elapsed),
381
- ].join(" · ");
382
- const metadata = activityMetadata(snapshot);
383
- const objective = sanitizeDisplayText(snapshot.objective, 240);
384
- const second = ` ${[objective, ...metadata].filter(Boolean).join(" · ")}`;
385
- const color = "tone" in snapshot && snapshot.tone ? snapshot.tone : "accent";
386
- return [
387
- line(styled(theme, color, first), width),
388
- line(styled(theme, "muted", second), width),
389
- ];
390
- }
391
-
392
- export function renderActivityWidgetLines(
393
- entries: readonly ActivitySnapshot[],
394
- width: number,
395
- _now = Date.now(),
396
- ): string[] {
397
- const ordered = [...entries].sort(
398
- (left, right) =>
399
- left.sequence - right.sequence ||
400
- left.toolCallId.localeCompare(right.toolCallId),
401
- );
402
- const visible = ordered.slice(0, 5);
403
- const hidden = ordered.slice(visible.length);
404
- const lines = [line(`Agents · ${ordered.length} active`, width)];
405
- for (const entry of visible) {
406
- lines.push(...renderActivityLines(entry, width, undefined, _now));
407
- }
408
- if (hidden.length > 0) {
409
- const running = hidden.filter((entry) => entry.state !== "queued").length;
410
- const queued = hidden.filter((entry) => entry.state === "queued").length;
411
- const overflow = [
412
- `+${hidden.length} more (${running} running, ${queued} queued)`,
413
- `+${hidden.length} (${running}r, ${queued}q)`,
414
- `+${hidden.length} ${running}r ${queued}q`,
415
- `+${hidden.length} ${running}/${queued}`,
416
- `${hidden.length}:${running}/${queued}`,
417
- `${running}/${queued}`,
418
- ].find((candidate) => visibleWidth(candidate) <= boundedWidth(width));
419
- lines.push(line(overflow ?? `${running}/${queued}`, width));
420
- }
421
- return lines
422
- .slice(0, ACTIVITY_WIDGET_MAX_LINES)
423
- .map((entry) => line(entry, width));
424
- }
425
-
426
- export class ActivityWidget implements Component {
427
- constructor(
428
- private readonly entries: () => readonly ActivitySnapshot[],
429
- private readonly now: () => number = Date.now,
430
- private readonly theme?: ActivityTheme,
431
- ) {}
432
-
433
- render(width: number): string[] {
434
- return renderActivityWidgetLines(this.entries(), width, this.now()).map(
435
- (entry, index) =>
436
- line(
437
- index === 0
438
- ? styled(this.theme, "accent", entry)
439
- : styled(this.theme, "text", entry),
440
- width,
441
- ),
442
- );
443
- }
444
-
445
- invalidate(): void {}
446
- }
447
-
448
- export class ActivityInlineComponent implements Component {
449
- constructor(
450
- private readonly display: ActivityDisplay | ActivitySnapshot,
451
- private readonly theme?: ActivityTheme,
452
- private readonly expanded = false,
453
- private readonly now: () => number = Date.now,
454
- ) {}
455
-
456
- render(width: number): string[] {
457
- const lines = renderActivityLines(
458
- this.display,
459
- width,
460
- this.theme,
461
- this.now(),
462
- );
463
- if (
464
- !this.expanded ||
465
- !("summary" in this.display) ||
466
- !this.display.summary
467
- ) {
468
- if ("reason" in this.display && this.display.reason) {
469
- lines[1] = line(
470
- styled(
471
- this.theme,
472
- this.display.tone,
473
- ` reason: ${sanitizeFailureReason(this.display.reason)}`,
474
- ),
475
- width,
476
- );
477
- }
478
- return lines;
479
- }
480
- const summary = this.display.summary;
481
- const detail =
482
- summary.kind === "evidence"
483
- ? ` evidence: ${summary.conclusions} conclusions · ${summary.citations} citations · ${summary.risks} risks · ${summary.blockingQuestions} blocking`
484
- : ` diff: ${sanitizeDisplayText(summary.summary)} · ${summary.riskCount} risks · retained: ${summary.retained ? "yes" : "no"}`;
485
- return [lines[0] ?? "", line(styled(this.theme, "muted", detail), width)];
486
- }
487
-
488
- invalidate(): void {}
489
- }
490
-
491
- function asRecord(value: unknown): Record<string, unknown> | undefined {
492
- return typeof value === "object" && value !== null
493
- ? (value as Record<string, unknown>)
494
- : undefined;
495
- }
496
-
497
- function hasAutomaticContinuation(result: Record<string, unknown>): boolean {
498
- const continuation = asRecord(result.continuation);
499
- return (
500
- ["paused", "approval-needed"].includes(String(result.state)) &&
501
- asRecord(result.pause)?.code !== "operation-cancelled" &&
502
- continuation?.owner === "parent" &&
503
- continuation.automatic === true &&
504
- (continuation.action === "amend" ||
505
- continuation.command === "resume" ||
506
- continuation.kind === "inspect-recovery")
507
- );
508
- }
509
-
510
- function workflowStateOf(
511
- result: Record<string, unknown>,
512
- ): WorkflowActivityState {
513
- const pause = asRecord(result.pause);
514
- const operation = asRecord(result.operation);
515
- if (
516
- pause?.code === "operation-cancelled" ||
517
- operation?.kind === "operation-cancelled" ||
518
- result.state === "operation-cancelled"
519
- ) {
520
- return "operation-cancelled";
521
- }
522
- if (hasAutomaticContinuation(result)) return "recovering";
523
- const state = result.state;
524
- switch (state) {
525
- case "not-started":
526
- case "created":
527
- case "ready":
528
- case "queued":
529
- return "queued";
530
- case "preparing":
531
- return "preparing";
532
- case "connecting":
533
- return "connecting";
534
- case "waiting-first-response":
535
- return "waiting-first-response";
536
- case "running":
537
- return "running";
538
- case "validating-delivery":
539
- case "validating":
540
- return "validating";
541
- case "retryable":
542
- case "retrying":
543
- return "retrying";
544
- case "verifying":
545
- case "change-verifying":
546
- case "ready-to-apply":
547
- return "verifying";
548
- case "paused":
549
- return "paused";
550
- case "approval-needed":
551
- return "approval-needed";
552
- case "applying":
553
- return "applying";
554
- case "recovering":
555
- return "recovering";
556
- case "discarded":
557
- return "discarded";
558
- case "rejected":
559
- return "rejected";
560
- case "completed":
561
- return result.completed === true ? "completed" : "rejected";
562
- default:
563
- return "running";
564
- }
565
- }
566
-
567
- function workflowTone(state: WorkflowActivityState): ActivityTone {
568
- switch (state) {
569
- case "completed":
570
- return "success";
571
- case "rejected":
572
- return "error";
573
- case "retrying":
574
- case "paused":
575
- case "approval-needed":
576
- return "warning";
577
- case "operation-cancelled":
578
- case "discarded":
579
- return "muted";
580
- default:
581
- return "accent";
582
- }
583
- }
584
-
585
- function legalWorkflowCommands(result: Record<string, unknown>): string[] {
586
- return Array.isArray(result.legalCommands)
587
- ? result.legalCommands.filter(
588
- (command): command is string =>
589
- typeof command === "string" && SAFE_CONTROL_ACTIONS.has(command),
590
- )
591
- : [];
592
- }
593
-
594
- function nextWorkflowAction(
595
- state: WorkflowActivityState,
596
- commands: readonly string[],
597
- ): string | undefined {
598
- const preferred =
599
- state === "paused" ||
600
- state === "approval-needed" ||
601
- state === "retrying" ||
602
- state === "operation-cancelled"
603
- ? ["resume", "status", "discard"]
604
- : state === "completed" || state === "discarded" || state === "rejected"
605
- ? ["status"]
606
- : ["status", "cancel", "discard"];
607
- return preferred.find((command) => commands.includes(command)) ?? commands[0];
608
- }
609
-
610
- function firstWorkflowTask(result: Record<string, unknown>):
611
- | {
612
- taskId?: string;
613
- phase?: string;
614
- }
615
- | undefined {
616
- if (!Array.isArray(result.tasks)) return undefined;
617
- const records = result.tasks
618
- .map(asRecord)
619
- .filter((task): task is Record<string, unknown> => task !== undefined);
620
- const task =
621
- records.find((candidate) => candidate.state !== "verified") ?? records[0];
622
- return task
623
- ? {
624
- ...(typeof task.taskId === "string" ? { taskId: task.taskId } : {}),
625
- ...(typeof task.phase === "string" ? { phase: task.phase } : {}),
626
- }
627
- : undefined;
628
- }
629
-
630
- function positiveInteger(value: unknown): number | undefined {
631
- return Number.isSafeInteger(value) && (value as number) > 0
632
- ? (value as number)
633
- : undefined;
634
- }
635
-
636
- function workflowWait(
637
- state: WorkflowActivityState,
638
- result: Record<string, unknown>,
639
- ): string | undefined {
640
- if (typeof result.wait === "string") {
641
- return sanitizeDisplayText(result.wait, 80);
642
- }
643
- const approval = asRecord(result.approval);
644
- if (
645
- state === "approval-needed" &&
646
- (approval?.gate === "gate-a" || approval?.gate === "gate-b")
647
- ) {
648
- return approval.gate;
649
- }
650
- if (state === "preparing") return "worker-setup";
651
- if (state === "connecting") return "connection";
652
- if (state === "waiting-first-response") return "first-response";
653
- if (state === "retrying") return "bounded-policy";
654
- const queue = Array.isArray(result.queue)
655
- ? asRecord(result.queue[0])
656
- : undefined;
657
- const position = positiveInteger(queue?.position);
658
- return state === "queued" && position ? `queue-${position}` : undefined;
659
- }
660
-
661
- function workflowCode(result: Record<string, unknown>): string | undefined {
662
- const pause = asRecord(result.pause);
663
- const raw =
664
- typeof result.code === "string"
665
- ? result.code
666
- : typeof pause?.code === "string"
667
- ? pause.code
668
- : undefined;
669
- return raw === undefined ? undefined : sanitizeWorkflowCode(raw);
670
- }
671
-
672
- export function projectWorkflowActivity(
673
- args: unknown,
674
- result: unknown,
675
- elapsedMs: number,
676
- ): ActivityDisplay {
677
- const command = asRecord(args) ?? {};
678
- const payload = asRecord(result) ?? {};
679
- const state = workflowStateOf(payload);
680
- const task = firstWorkflowTask(payload);
681
- const stage =
682
- payload.stage === "abel-design" || payload.stage === "abel-implement"
683
- ? payload.stage
684
- : command.stage === "abel-design" || command.stage === "abel-implement"
685
- ? command.stage
686
- : "abel-control";
687
- const change =
688
- typeof payload.change === "string"
689
- ? payload.change
690
- : typeof command.change === "string"
691
- ? command.change
692
- : undefined;
693
- const runId = typeof payload.runId === "string" ? payload.runId : undefined;
694
- const taskId =
695
- typeof payload.taskId === "string" ? payload.taskId : task?.taskId;
696
- const phase =
697
- typeof payload.phase === "string"
698
- ? payload.phase
699
- : (task?.phase ??
700
- (typeof command.command === "string" ? command.command : "control"));
701
- const objective =
702
- typeof payload.objective === "string"
703
- ? payload.objective
704
- : change
705
- ? `change ${change}`
706
- : `${String(command.command ?? "control")} workflow run`;
707
- const legalCommands = legalWorkflowCommands(payload);
708
- const policy = asRecord(payload.policy);
709
- const attempt = positiveInteger(payload.attempt ?? policy?.attempt);
710
- const maxAttempts = positiveInteger(
711
- payload.maxAttempts ?? policy?.maxAttempts,
712
- );
713
- const code =
714
- state === "rejected" && payload.state === "completed"
715
- ? "completion-state-inconsistent"
716
- : workflowCode(payload);
717
- return {
718
- kind: "activityDisplay",
719
- requestId: sanitizeDisplayText(
720
- runId ?? taskId ?? change ?? command.operationId ?? "pending",
721
- 128,
722
- ),
723
- role: sanitizeDisplayText(stage, 80),
724
- phase: sanitizeDisplayText(phase, 40),
725
- objective: sanitizeDisplayText(objective, 240),
726
- state,
727
- tone: workflowTone(state),
728
- elapsedMs: Math.max(0, elapsedMs),
729
- ...(runId ? { runId: sanitizeDisplayText(runId, 128) } : {}),
730
- ...(taskId ? { taskId: sanitizeDisplayText(taskId, 128) } : {}),
731
- ...(code ? { code } : {}),
732
- ...(attempt ? { attempt } : {}),
733
- ...(maxAttempts ? { maxAttempts } : {}),
734
- ...(workflowWait(state, payload)
735
- ? { wait: workflowWait(state, payload) }
736
- : {}),
737
- ...(!hasAutomaticContinuation(payload) &&
738
- nextWorkflowAction(state, legalCommands)
739
- ? { nextAction: nextWorkflowAction(state, legalCommands) }
740
- : {}),
741
- };
742
- }
743
-
744
- function activityResultFromUpdate(update: WorkflowActivityUpdate): unknown {
745
- return {
746
- ...update,
747
- ...(update.state === "completed" ? { completed: true } : {}),
748
- ...(update.code ? { pause: { code: update.code } } : {}),
749
- };
750
- }
751
-
752
- export function summarizeDispatchResult(
753
- result: unknown,
754
- ): ActivitySummary | undefined {
755
- const outer = asRecord(result);
756
- if (!outer || outer.ok === false) return undefined;
757
- const outcome = outer.ok === true ? asRecord(outer.result) : outer;
758
- if (!outcome) return undefined;
759
- const value =
760
- outcome.kind === "candidate" ? asRecord(outcome.result) : outcome;
761
- if (!value) return undefined;
762
- if (
763
- value.kind === "evidence" &&
764
- Array.isArray(value.conclusions) &&
765
- Array.isArray(value.citations) &&
766
- Array.isArray(value.risks) &&
767
- Array.isArray(value.blockingQuestions)
768
- ) {
769
- return {
770
- kind: "evidence",
771
- conclusions: value.conclusions.length,
772
- citations: value.citations.length,
773
- risks: value.risks.length,
774
- blockingQuestions: value.blockingQuestions.length,
775
- };
776
- }
777
- if (
778
- value.kind === "evidence" &&
779
- Array.isArray(value.evidence) &&
780
- Array.isArray(value.existing_structures) &&
781
- Array.isArray(value.risks) &&
782
- Array.isArray(value.open_questions)
783
- ) {
784
- return {
785
- kind: "evidence",
786
- conclusions: value.existing_structures.length,
787
- citations: value.evidence.length,
788
- risks: value.risks.length,
789
- blockingQuestions: value.open_questions.length,
790
- };
791
- }
792
- if (
793
- value.kind === "diff" &&
794
- typeof value.summary === "string" &&
795
- Array.isArray(value.risks)
796
- ) {
797
- return {
798
- kind: "diff",
799
- summary: sanitizeDisplayText(value.summary),
800
- riskCount: value.risks.length,
801
- retained:
802
- typeof outer.resultId === "string" ||
803
- typeof outcome.resultId === "string",
804
- };
805
- }
806
- return undefined;
807
- }
808
-
809
- function toneForResult(result: unknown): ActivityTone | undefined {
810
- const outer = asRecord(result);
811
- if (!outer || outer.ok === false) return undefined;
812
- const value = outer.ok === true ? asRecord(outer.result) : outer;
813
- switch (value?.kind) {
814
- case "candidate":
815
- case "applied":
816
- case "completed":
817
- return "success";
818
- case "deferred":
819
- case "retry":
820
- case "checkpoint-required":
821
- return "warning";
822
- case "blocked":
823
- case "cancelled":
824
- return "muted";
825
- default:
826
- return undefined;
827
- }
828
- }
829
-
830
- function terminalState(
831
- event: PacketActivityEvent,
832
- ): event is PacketActivityEvent & {
833
- state: TerminalActivityState;
834
- } {
835
- return ![
836
- "queued",
837
- "preparing",
838
- "connecting",
839
- "waiting-first-response",
840
- "running",
841
- "retrying",
842
- ].includes(event.state);
843
- }
844
-
845
- function displayFromEvent(
846
- entry: ActivityEntry,
847
- event: PacketActivityEvent,
848
- elapsedMs: number,
849
- ): ActivityDisplay {
850
- const reason = event.failureReason;
851
- return {
852
- kind: "activityDisplay",
853
- requestId: entry.requestId,
854
- role: entry.role,
855
- phase: entry.phase,
856
- objective: sanitizeDisplayText(entry.objective),
857
- state: event.state as TerminalActivityState,
858
- ...(entry.attempt ? { attempt: entry.attempt } : {}),
859
- ...(entry.maxAttempts ? { maxAttempts: entry.maxAttempts } : {}),
860
- ...(entry.code ? { code: entry.code } : {}),
861
- ...(entry.wait ? { wait: entry.wait } : {}),
862
- tone:
863
- event.state === "failed" || event.state === "timed-out"
864
- ? "error"
865
- : event.state === "cancelled"
866
- ? "muted"
867
- : "success",
868
- elapsedMs,
869
- ...(reason === undefined ? {} : { reason: sanitizeFailureReason(reason) }),
870
- };
871
- }
872
-
873
- export function createActivityDisplay(
874
- event: PacketActivityEvent,
875
- elapsedMs: number,
876
- result?: unknown,
877
- ): ActivityDisplay {
878
- const entry: ActivityEntry = {
879
- toolCallId: "",
880
- requestId: event.requestId,
881
- role: event.role,
882
- phase: event.phase,
883
- objective: event.objective,
884
- state: event.state,
885
- sequence: event.sequence,
886
- startedAt: 0,
887
- elapsedMs,
888
- ...(event.attempt ? { attempt: event.attempt } : {}),
889
- ...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
890
- ...(event.code ? { code: event.code } : {}),
891
- ...(event.wait ? { wait: event.wait } : {}),
892
- };
893
- const display = displayFromEvent(entry, event, elapsedMs);
894
- const summary = result ? summarizeDispatchResult(result) : undefined;
895
- const tone = result ? toneForResult(result) : undefined;
896
- return {
897
- ...display,
898
- ...(tone === undefined ? {} : { tone }),
899
- ...(summary === undefined ? {} : { summary }),
900
- };
901
- }
902
-
903
- function activityPartial(display: ActivityDisplay | ActivitySnapshot): unknown {
904
- const safeDisplay =
905
- "onUpdate" in display
906
- ? (({ onUpdate: _onUpdate, ...snapshot }) => snapshot)(display)
907
- : display;
908
- return {
909
- content: [
910
- {
911
- type: "text",
912
- text: renderActivityLines(safeDisplay, 120).join("\n"),
913
- },
914
- ],
915
- details: { [ACTIVITY_DETAILS_KEY]: safeDisplay },
916
- };
917
- }
918
-
919
- export class ActivityController {
920
- private ui?: ActivityUi;
921
- private accepting = false;
922
- private readonly entries = new Map<string, ActivityEntry>();
923
- private readonly terminals = new Map<string, ActivityDisplay>();
924
- private spinnerFrame = 0;
925
- private timer?: TimerHandle | unknown;
926
- private requestRender?: () => void;
927
- private widgetInstalled = false;
928
- private controlSequence = 0;
929
- private readonly now: () => number;
930
- private readonly setIntervalFn: (
931
- callback: () => void,
932
- delay: number,
933
- ) => TimerHandle | unknown;
934
- private readonly clearIntervalFn: (handle: TimerHandle | unknown) => void;
935
-
936
- constructor(options: ActivityControllerOptions = {}) {
937
- this.now = options.now ?? Date.now;
938
- this.setIntervalFn =
939
- options.setInterval ??
940
- ((callback, delay) => setInterval(callback, delay));
941
- this.clearIntervalFn =
942
- options.clearInterval ??
943
- ((handle) => clearInterval(handle as TimerHandle));
944
- }
945
-
946
- attach(ui: ActivityUi): void {
947
- this.detachUi();
948
- this.ui = ui;
949
- this.accepting = true;
950
- }
951
-
952
- detach(): void {
953
- this.accepting = false;
954
- this.entries.clear();
955
- this.terminals.clear();
956
- this.stopTimer();
957
- this.detachUi();
958
- this.ui = undefined;
959
- }
960
-
961
- clear(): void {
962
- this.entries.clear();
963
- this.terminals.clear();
964
- this.stopTimer();
965
- this.clearUi();
966
- }
967
-
968
- beginWorkflow(
969
- toolCallId: string,
970
- args: unknown,
971
- onUpdate?: (result: unknown) => void,
972
- ): void {
973
- if (!this.accepting || this.entries.has(toolCallId)) return;
974
- const display = projectWorkflowActivity(args, { state: "queued" }, 0);
975
- const entry: ActivityEntry = {
976
- toolCallId,
977
- requestId: display.requestId,
978
- role: display.role,
979
- phase: display.phase,
980
- objective: display.objective,
981
- state: display.state,
982
- sequence: ++this.controlSequence,
983
- startedAt: this.now(),
984
- elapsedMs: 0,
985
- spinnerFrame: this.spinnerFrame,
986
- tone: display.tone,
987
- onUpdate,
988
- };
989
- this.entries.set(toolCallId, entry);
990
- this.emit(entry, entry);
991
- this.syncUi();
992
- }
993
-
994
- updateWorkflow(
995
- toolCallId: string,
996
- args: unknown,
997
- update: WorkflowActivityUpdate,
998
- ): void {
999
- const entry = this.entries.get(toolCallId);
1000
- if (!this.accepting || !entry) return;
1001
- const display = projectWorkflowActivity(
1002
- args,
1003
- activityResultFromUpdate(update),
1004
- Math.max(0, this.now() - entry.startedAt),
1005
- );
1006
- Object.assign(entry, {
1007
- requestId: display.requestId,
1008
- role: display.role,
1009
- phase: display.phase,
1010
- objective: display.objective,
1011
- state: display.state,
1012
- elapsedMs: display.elapsedMs,
1013
- tone: display.tone,
1014
- runId: display.runId,
1015
- taskId: display.taskId,
1016
- code: display.code,
1017
- nextAction: display.nextAction,
1018
- attempt: display.attempt,
1019
- maxAttempts: display.maxAttempts,
1020
- wait: display.wait,
1021
- });
1022
- this.emit(entry, entry);
1023
- this.syncUi();
1024
- }
1025
-
1026
- finalizeWorkflow(
1027
- toolCallId: string,
1028
- args: unknown,
1029
- result: unknown,
1030
- ): ActivityDisplay | undefined {
1031
- const entry = this.entries.get(toolCallId);
1032
- if (!entry) return undefined;
1033
- const display = projectWorkflowActivity(
1034
- args,
1035
- result,
1036
- Math.max(0, this.now() - entry.startedAt),
1037
- );
1038
- this.emit(entry, display);
1039
- this.entries.delete(toolCallId);
1040
- this.syncUi();
1041
- return display;
1042
- }
1043
-
1044
- failWorkflow(toolCallId: string): void {
1045
- const entry = this.entries.get(toolCallId);
1046
- if (!entry) return;
1047
- const display: ActivityDisplay = {
1048
- kind: "activityDisplay",
1049
- requestId: entry.requestId,
1050
- role: entry.role,
1051
- phase: entry.phase,
1052
- objective: entry.objective,
1053
- state: "failed",
1054
- tone: "error",
1055
- elapsedMs: Math.max(0, this.now() - entry.startedAt),
1056
- reason: "subagent failed",
1057
- };
1058
- this.emit(entry, display);
1059
- this.entries.delete(toolCallId);
1060
- this.syncUi();
1061
- }
1062
-
1063
- observe(
1064
- toolCallId: string,
1065
- onUpdate?: (result: unknown) => void,
1066
- ): PacketActivityObserver {
1067
- return (event) => this.accept(toolCallId, onUpdate, event);
1068
- }
1069
-
1070
- accept(
1071
- toolCallId: string,
1072
- onUpdate: ((result: unknown) => void) | undefined,
1073
- event: PacketActivityEvent,
1074
- ): void {
1075
- if (!this.accepting || this.terminals.has(toolCallId)) return;
1076
- let entry = this.entries.get(toolCallId);
1077
- if (!entry) {
1078
- entry = {
1079
- toolCallId,
1080
- requestId: event.requestId,
1081
- role: event.role,
1082
- phase: event.phase,
1083
- objective: sanitizeDisplayText(event.objective),
1084
- state: event.state,
1085
- sequence: event.sequence,
1086
- startedAt: this.now(),
1087
- elapsedMs: 0,
1088
- spinnerFrame: this.spinnerFrame,
1089
- ...(event.attempt ? { attempt: event.attempt } : {}),
1090
- ...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
1091
- ...(event.code ? { code: event.code } : {}),
1092
- ...(event.wait ? { wait: event.wait } : {}),
1093
- onUpdate,
1094
- };
1095
- this.entries.set(toolCallId, entry);
1096
- } else {
1097
- entry.onUpdate = onUpdate ?? entry.onUpdate;
1098
- entry.state = event.state;
1099
- entry.attempt = event.attempt;
1100
- entry.maxAttempts = event.maxAttempts;
1101
- entry.code = event.code;
1102
- entry.wait = event.wait;
1103
- }
1104
- if (terminalState(event)) {
1105
- const display = displayFromEvent(
1106
- entry,
1107
- event,
1108
- Math.max(0, this.now() - entry.startedAt),
1109
- );
1110
- this.terminals.set(toolCallId, display);
1111
- this.emit(entry, display);
1112
- this.entries.delete(toolCallId);
1113
- this.syncUi();
1114
- return;
1115
- }
1116
- this.emit(entry, {
1117
- ...entry,
1118
- elapsedMs: Math.max(0, this.now() - entry.startedAt),
1119
- });
1120
- this.syncUi();
1121
- }
1122
-
1123
- finalize(toolCallId: string, result: unknown): ActivityDisplay | undefined {
1124
- const display = this.terminals.get(toolCallId);
1125
- if (!display) return undefined;
1126
- this.terminals.delete(toolCallId);
1127
- const summary = summarizeDispatchResult(result);
1128
- const tone = toneForResult(result);
1129
- return {
1130
- ...display,
1131
- ...(tone === undefined ? {} : { tone }),
1132
- ...(summary === undefined ? {} : { summary }),
1133
- };
1134
- }
1135
-
1136
- getActiveEntries(): readonly ActivitySnapshot[] {
1137
- return [...this.entries.values()].map(
1138
- ({ onUpdate: _onUpdate, ...entry }) => ({
1139
- ...entry,
1140
- elapsedMs: Math.max(0, this.now() - entry.startedAt),
1141
- spinnerFrame: this.spinnerFrame,
1142
- }),
1143
- );
1144
- }
1145
-
1146
- isTimerActive(): boolean {
1147
- return this.timer !== undefined;
1148
- }
1149
-
1150
- private emit(
1151
- entry: ActivityEntry,
1152
- display: ActivityDisplay | ActivitySnapshot,
1153
- ): void {
1154
- try {
1155
- entry.onUpdate?.(activityPartial(display));
1156
- } catch {
1157
- // A renderer callback is not part of orchestration.
1158
- }
1159
- }
1160
-
1161
- private ensureWidget(): void {
1162
- if (!this.ui || this.widgetInstalled) return;
1163
- try {
1164
- this.ui.setWidget(
1165
- ACTIVITY_WIDGET_KEY,
1166
- (tui, theme) => {
1167
- this.requestRender = () => tui.requestRender();
1168
- return new ActivityWidget(
1169
- () => this.getActiveEntries(),
1170
- this.now,
1171
- theme,
1172
- );
1173
- },
1174
- { placement: "aboveEditor" },
1175
- );
1176
- this.widgetInstalled = true;
1177
- } catch {
1178
- // TUI failures are deliberately isolated from the run.
1179
- }
1180
- }
1181
-
1182
- private startTimer(): void {
1183
- if (this.timer !== undefined) return;
1184
- try {
1185
- this.timer = this.setIntervalFn(
1186
- () => this.tick(),
1187
- ACTIVITY_REFRESH_MS,
1188
- ) as TimerHandle;
1189
- } catch {
1190
- this.timer = undefined;
1191
- }
1192
- }
1193
-
1194
- private stopTimer(): void {
1195
- if (this.timer === undefined) return;
1196
- try {
1197
- this.clearIntervalFn(this.timer);
1198
- } catch {
1199
- // The timer is best-effort presentation state.
1200
- }
1201
- this.timer = undefined;
1202
- }
1203
-
1204
- private tick(): void {
1205
- if (this.entries.size === 0) {
1206
- this.stopTimer();
1207
- this.clearUi();
1208
- return;
1209
- }
1210
- this.spinnerFrame++;
1211
- for (const entry of this.entries.values()) {
1212
- this.emit(entry, {
1213
- ...entry,
1214
- elapsedMs: Math.max(0, this.now() - entry.startedAt),
1215
- spinnerFrame: this.spinnerFrame,
1216
- });
1217
- }
1218
- try {
1219
- this.requestRender?.();
1220
- } catch {
1221
- // Widget refresh is best-effort.
1222
- }
1223
- this.syncUi();
1224
- }
1225
-
1226
- private syncUi(): void {
1227
- if (!this.ui) return;
1228
- if (this.entries.size === 0) {
1229
- this.stopTimer();
1230
- this.clearUi();
1231
- return;
1232
- }
1233
- this.ensureWidget();
1234
- this.startTimer();
1235
- try {
1236
- const running = [...this.entries.values()].filter(
1237
- (entry) => entry.state !== "queued",
1238
- ).length;
1239
- const queued = this.entries.size - running;
1240
- this.ui.setStatus(
1241
- ACTIVITY_STATUS_KEY,
1242
- `Agents: ${running} running, ${queued} queued`,
1243
- );
1244
- } catch {
1245
- // Status rendering is optional.
1246
- }
1247
- }
1248
-
1249
- private clearUi(): void {
1250
- if (!this.ui) return;
1251
- try {
1252
- if (this.widgetInstalled)
1253
- this.ui.setWidget(ACTIVITY_WIDGET_KEY, undefined);
1254
- } catch {
1255
- // Best-effort cleanup.
1256
- }
1257
- try {
1258
- this.ui.setStatus(ACTIVITY_STATUS_KEY, undefined);
1259
- } catch {
1260
- // Best-effort cleanup.
1261
- }
1262
- this.widgetInstalled = false;
1263
- this.requestRender = undefined;
1264
- }
1265
-
1266
- private detachUi(): void {
1267
- this.clearUi();
1268
- }
1269
- }
1270
-
1271
- export function renderActivityResult(
1272
- result: unknown,
1273
- options: ToolRenderResultOptions,
1274
- theme?: ActivityTheme,
1275
- context?: ActivityRenderContext,
1276
- ): Component {
1277
- const details = asRecord(asRecord(result)?.details);
1278
- const activity = details?.[ACTIVITY_DETAILS_KEY];
1279
- const failed = context?.isError === true;
1280
- const args = asRecord(context?.args);
1281
- const failedRun = failed && args?.action === "run";
1282
- if (isActivityDisplay(activity) && !failed) {
1283
- return new ActivityInlineComponent(activity, theme, options.expanded);
1284
- }
1285
- if (failedRun) {
1286
- const snapshot = identityFromDispatchArgs(asRecord(context?.args)?.request);
1287
- const display: ActivityDisplay = {
1288
- kind: "activityDisplay",
1289
- requestId: isActivityDisplay(activity)
1290
- ? activity.requestId
1291
- : snapshot.requestId,
1292
- role: isActivityDisplay(activity) ? activity.role : snapshot.role,
1293
- phase: isActivityDisplay(activity) ? activity.phase : snapshot.phase,
1294
- objective: isActivityDisplay(activity)
1295
- ? activity.objective
1296
- : snapshot.objective,
1297
- state: "failed",
1298
- tone: "error",
1299
- elapsedMs: isActivityDisplay(activity) ? activity.elapsedMs : 0,
1300
- reason: sanitizeFailureReason(
1301
- isActivityDisplay(activity) ? activity.reason : undefined,
1302
- ),
1303
- };
1304
- return new ActivityInlineComponent(display, theme);
1305
- }
1306
- if (failed) {
1307
- const designFailure = safeDesignFailure(details?.designFailure);
1308
- if (args?.action === "design" && designFailure) {
1309
- return renderDesignFailure(designFailure, options.expanded, theme);
1310
- }
1311
- return new Text(
1312
- `${dispatchLabel(args?.action ?? args?.command)} failed`,
1313
- 0,
1314
- 0,
1315
- );
1316
- }
1317
- const content = asRecord(result)?.content;
1318
- const text = Array.isArray(content)
1319
- ? content
1320
- .map((item) => asRecord(item)?.text)
1321
- .filter((item): item is string => typeof item === "string")
1322
- .join("\n")
1323
- : "";
1324
- return new Text(text, 0, 0);
1325
- }
1326
-
1327
- function dispatchLabel(action: unknown): string {
1328
- const value = sanitizeDisplayText(action ?? "");
1329
- return value.length > 0 ? `Abel Dispatch ${value}` : "Abel Dispatch";
1330
- }
1331
-
1332
- function isActivityDisplay(value: unknown): value is ActivityDisplay {
1333
- const record = asRecord(value);
1334
- return (
1335
- record?.kind === "activityDisplay" &&
1336
- typeof record.requestId === "string" &&
1337
- typeof record.role === "string" &&
1338
- typeof record.phase === "string" &&
1339
- typeof record.objective === "string" &&
1340
- typeof record.state === "string" &&
1341
- typeof record.elapsedMs === "number"
1342
- );
1343
- }
1344
-
1345
- function identityFromDispatchArgs(request: unknown): {
1346
- requestId: string;
1347
- role: string;
1348
- phase: string;
1349
- objective: string;
1350
- } {
1351
- const value = asRecord(request);
1352
- const attempt = asRecord(value?.attempt);
1353
- const boundary = asRecord(value?.boundary);
1354
- return {
1355
- requestId: sanitizeDisplayText(
1356
- attempt?.requestId ?? value?.id ?? "unknown",
1357
- 128,
1358
- ),
1359
- role: sanitizeDisplayText(
1360
- value?.role ??
1361
- (value?.stage === "abel-implement"
1362
- ? "implementation-worker"
1363
- : "unknown"),
1364
- 80,
1365
- ),
1366
- phase: sanitizeDisplayText(attempt?.phase ?? value?.phase ?? "unknown", 40),
1367
- objective: sanitizeDisplayText(
1368
- value?.objective ?? boundary?.objective ?? "",
1369
- 240,
1370
- ),
1371
- };
1372
- }
1373
-
1374
- export function renderActivityCall(
1375
- args: unknown,
1376
- theme?: ActivityTheme,
1377
- context?: ActivityRenderContext,
1378
- ): Component {
1379
- const value = asRecord(args);
1380
- if (typeof value?.command === "string") {
1381
- if (context?.executionStarted || context?.isError) return new Text("");
1382
- const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
1383
- const display: ActivitySnapshot = {
1384
- toolCallId: "call",
1385
- requestId: projected.requestId,
1386
- role: projected.role,
1387
- phase: projected.phase,
1388
- objective: projected.objective,
1389
- state: projected.state,
1390
- sequence: 0,
1391
- startedAt: Date.now(),
1392
- elapsedMs: 0,
1393
- tone: projected.tone,
1394
- };
1395
- return new ActivityInlineComponent(display, theme);
1396
- }
1397
- if (value?.action !== "run") {
1398
- return new Text(context?.isError ? "" : dispatchLabel(value?.action), 0, 0);
1399
- }
1400
- if (context?.executionStarted || context?.isError) {
1401
- return new Text("");
1402
- }
1403
- if (!asRecord(value.request)) return new Text("Subagent", 0, 0);
1404
- const identity = identityFromDispatchArgs(value.request);
1405
- const display: ActivitySnapshot = {
1406
- toolCallId: "call",
1407
- requestId: identity.requestId,
1408
- role: identity.role,
1409
- phase: identity.phase,
1410
- objective: identity.objective,
1411
- state: "queued",
1412
- sequence: 0,
1413
- startedAt: Date.now(),
1414
- elapsedMs: 0,
1415
- };
1416
- return new ActivityInlineComponent(display, theme);
1417
- }