@abelxiaoxing/cadence 1.8.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. 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
- }