@selesai/code 0.1.5 → 0.1.7

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 (143) hide show
  1. package/dist/config.d.ts +23 -4
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +100 -5
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/package-manager.d.ts +1 -0
  6. package/dist/core/package-manager.d.ts.map +1 -1
  7. package/dist/core/package-manager.js +6 -1
  8. package/dist/core/package-manager.js.map +1 -1
  9. package/dist/core/resource-loader.d.ts.map +1 -1
  10. package/dist/core/resource-loader.js +3 -2
  11. package/dist/core/resource-loader.js.map +1 -1
  12. package/dist/core/settings-manager.d.ts +1 -0
  13. package/dist/core/settings-manager.d.ts.map +1 -1
  14. package/dist/core/settings-manager.js.map +1 -1
  15. package/dist/defaults/settings.json +2 -4
  16. package/dist/extensions/pi-intercom/LICENSE +21 -0
  17. package/dist/extensions/pi-intercom/README.md +484 -0
  18. package/dist/extensions/pi-intercom/broker/broker.ts +345 -0
  19. package/dist/extensions/pi-intercom/broker/client.ts +535 -0
  20. package/dist/extensions/pi-intercom/broker/framing.ts +57 -0
  21. package/dist/extensions/pi-intercom/broker/paths.ts +20 -0
  22. package/dist/extensions/pi-intercom/broker/spawn.ts +307 -0
  23. package/dist/extensions/pi-intercom/config.ts +108 -0
  24. package/dist/extensions/pi-intercom/index.ts +1778 -0
  25. package/dist/extensions/pi-intercom/package.json +38 -0
  26. package/dist/extensions/pi-intercom/reply-tracker.ts +102 -0
  27. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +512 -0
  28. package/dist/extensions/pi-intercom/types.ts +46 -0
  29. package/dist/extensions/pi-intercom/ui/compose.ts +139 -0
  30. package/dist/extensions/pi-intercom/ui/inline-message.ts +76 -0
  31. package/dist/extensions/pi-intercom/ui/session-list.ts +162 -0
  32. package/dist/extensions/pi-rewind-hook/README.md +245 -0
  33. package/dist/extensions/pi-rewind-hook/index.ts +1445 -0
  34. package/dist/extensions/pi-rewind-hook/package.json +29 -0
  35. package/dist/extensions/pi-subagents/README.md +1290 -0
  36. package/dist/extensions/pi-subagents/agents/context-builder.md +46 -0
  37. package/dist/extensions/pi-subagents/agents/delegate.md +12 -0
  38. package/dist/extensions/pi-subagents/agents/oracle.md +73 -0
  39. package/dist/extensions/pi-subagents/agents/planner.md +55 -0
  40. package/dist/extensions/pi-subagents/agents/researcher.md +52 -0
  41. package/dist/extensions/pi-subagents/agents/reviewer.md +79 -0
  42. package/dist/extensions/pi-subagents/agents/scout.md +50 -0
  43. package/dist/extensions/pi-subagents/agents/worker.md +55 -0
  44. package/dist/extensions/pi-subagents/package.json +83 -0
  45. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +13 -0
  46. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +59 -0
  47. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +55 -0
  48. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +61 -0
  49. package/dist/extensions/pi-subagents/prompts/parallel-research.md +50 -0
  50. package/dist/extensions/pi-subagents/prompts/parallel-review.md +54 -0
  51. package/dist/extensions/pi-subagents/prompts/review-loop.md +41 -0
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +855 -0
  53. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +875 -0
  54. package/dist/extensions/pi-subagents/src/agents/agent-scope.ts +6 -0
  55. package/dist/extensions/pi-subagents/src/agents/agent-selection.ts +25 -0
  56. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +110 -0
  57. package/dist/extensions/pi-subagents/src/agents/agents.ts +1379 -0
  58. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +252 -0
  59. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +93 -0
  60. package/dist/extensions/pi-subagents/src/agents/identity.ts +30 -0
  61. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +191 -0
  62. package/dist/extensions/pi-subagents/src/agents/skills.ts +729 -0
  63. package/dist/extensions/pi-subagents/src/extension/config.ts +16 -0
  64. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +92 -0
  65. package/dist/extensions/pi-subagents/src/extension/doctor.ts +220 -0
  66. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +171 -0
  67. package/dist/extensions/pi-subagents/src/extension/index.ts +627 -0
  68. package/dist/extensions/pi-subagents/src/extension/schemas.ts +265 -0
  69. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +412 -0
  70. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +377 -0
  71. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +637 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +916 -0
  73. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +355 -0
  74. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +391 -0
  75. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +338 -0
  76. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +282 -0
  77. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +161 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +63 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +177 -0
  80. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +108 -0
  81. package/dist/extensions/pi-subagents/src/runs/background/parallel-groups.ts +45 -0
  82. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +316 -0
  83. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +83 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +273 -0
  85. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +341 -0
  86. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +2458 -0
  87. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +13 -0
  88. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1333 -0
  89. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1217 -0
  90. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +1092 -0
  91. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +3125 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +856 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +116 -0
  94. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +147 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +295 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +175 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +141 -0
  99. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +819 -0
  100. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +52 -0
  101. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +115 -0
  102. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +149 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +240 -0
  104. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +115 -0
  105. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +60 -0
  106. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +178 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +77 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +223 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +222 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +206 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +577 -0
  112. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +98 -0
  113. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +73 -0
  114. package/dist/extensions/pi-subagents/src/shared/file-coalescer.ts +40 -0
  115. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +76 -0
  116. package/dist/extensions/pi-subagents/src/shared/formatters.ts +133 -0
  117. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +81 -0
  118. package/dist/extensions/pi-subagents/src/shared/model-info.ts +78 -0
  119. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +85 -0
  120. package/dist/extensions/pi-subagents/src/shared/session-identity.ts +10 -0
  121. package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +44 -0
  122. package/dist/extensions/pi-subagents/src/shared/settings.ts +448 -0
  123. package/dist/extensions/pi-subagents/src/shared/status-format.ts +49 -0
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +1064 -0
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +469 -0
  126. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +420 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +176 -0
  128. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1165 -0
  129. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +292 -0
  130. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +80 -0
  131. package/dist/extensions/pi-subagents/src/tui/render.ts +1728 -0
  132. package/dist/extensions/quick.ts +630 -0
  133. package/dist/modes/index.d.ts +1 -1
  134. package/dist/modes/index.d.ts.map +1 -1
  135. package/dist/modes/index.js.map +1 -1
  136. package/dist/modes/rpc/rpc-mode.d.ts +1 -1
  137. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  138. package/dist/modes/rpc/rpc-mode.js +4 -3
  139. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/dist/modes/rpc/rpc-types.d.ts +3 -2
  141. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/package.json +1 -1
@@ -0,0 +1,355 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
5
+ import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
+ import {
7
+ type AsyncJobState,
8
+ type AsyncStartedEvent,
9
+ type ControlEvent,
10
+ type SubagentState,
11
+ POLL_INTERVAL_MS,
12
+ RESULTS_DIR,
13
+ SUBAGENT_CONTROL_EVENT,
14
+ SUBAGENT_CONTROL_INTERCOM_EVENT,
15
+ } from "../../shared/types.ts";
16
+ import { readStatus } from "../../shared/utils.ts";
17
+ import { normalizeParallelGroups } from "./parallel-groups.ts";
18
+ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
19
+ import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
20
+
21
+ interface AsyncJobTrackerOptions {
22
+ completionRetentionMs?: number;
23
+ pollIntervalMs?: number;
24
+ resultsDir?: string;
25
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
26
+ now?: () => number;
27
+ }
28
+
29
+ const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
30
+ const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
31
+ const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
32
+
33
+ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
34
+ ensurePoller: () => void;
35
+ handleStarted: (data: unknown) => void;
36
+ handleComplete: (data: unknown) => void;
37
+ resetJobs: (ctx?: ExtensionContext) => void;
38
+ } {
39
+ const completionRetentionMs = options.completionRetentionMs ?? 10000;
40
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
41
+ const resultsDir = options.resultsDir ?? RESULTS_DIR;
42
+ const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
43
+ renderWidget(ctx, jobs);
44
+ ctx.ui.requestRender?.();
45
+ };
46
+ const cancelCleanup = (asyncId: string) => {
47
+ const existingTimer = state.cleanupTimers.get(asyncId);
48
+ if (!existingTimer) return;
49
+ clearTimeout(existingTimer);
50
+ state.cleanupTimers.delete(asyncId);
51
+ };
52
+ const scheduleCleanup = (asyncId: string) => {
53
+ cancelCleanup(asyncId);
54
+ const timer = setTimeout(() => {
55
+ state.cleanupTimers.delete(asyncId);
56
+ state.asyncJobs.delete(asyncId);
57
+ if (state.lastUiContext) {
58
+ rerenderWidget(state.lastUiContext);
59
+ }
60
+ }, completionRetentionMs);
61
+ state.cleanupTimers.set(asyncId, timer);
62
+ };
63
+ const emitNewControlEvents = (job: AsyncJobState) => {
64
+ const eventsPath = path.join(job.asyncDir, "events.jsonl");
65
+ let fd: number;
66
+ try {
67
+ fd = fs.openSync(eventsPath, "r");
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
70
+ console.error(`Failed to open async control events for '${job.asyncDir}':`, error);
71
+ return;
72
+ }
73
+ try {
74
+ const stat = fs.fstatSync(fd);
75
+ const savedCursor = job.controlEventCursor;
76
+ let cursor = stat.size < (savedCursor ?? 0) ? 0 : (savedCursor ?? 0);
77
+ const startedFromTail = savedCursor === undefined && stat.size > CONTROL_EVENT_SCAN_WINDOW_BYTES;
78
+ if (startedFromTail) cursor = stat.size - CONTROL_EVENT_SCAN_WINDOW_BYTES;
79
+ if (stat.size <= cursor) return;
80
+ const scanEnd = Math.min(stat.size, cursor + CONTROL_EVENT_SCAN_WINDOW_BYTES);
81
+ const handleLine = (line: string) => {
82
+ if (!line.trim()) return;
83
+ let parsed: unknown;
84
+ try {
85
+ parsed = JSON.parse(line);
86
+ } catch (error) {
87
+ console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
88
+ return;
89
+ }
90
+ if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") return;
91
+ const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
92
+ if (!record.event || !Array.isArray(record.channels)) return;
93
+ const payload = {
94
+ event: record.event,
95
+ source: "async" as const,
96
+ asyncDir: job.asyncDir,
97
+ childIntercomTarget: record.childIntercomTarget,
98
+ noticeText: record.noticeText ?? formatControlNoticeMessage(record.event, record.childIntercomTarget),
99
+ };
100
+ if (record.channels.includes("event")) {
101
+ pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
102
+ }
103
+ if (record.event.type !== "active_long_running" && record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
104
+ pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
105
+ ...payload,
106
+ to: record.intercom.to,
107
+ message: record.intercom.message,
108
+ });
109
+ }
110
+ };
111
+ let readCursor = cursor;
112
+ let lastCompleteCursor = cursor;
113
+ let lineParts: Buffer[] = [];
114
+ let lineBytes = 0;
115
+ let skippingOversizedLine = startedFromTail;
116
+ const appendLineSegment = (segment: Buffer) => {
117
+ if (segment.length === 0 || skippingOversizedLine) return;
118
+ if (lineBytes + segment.length > MAX_CONTROL_EVENT_LINE_BYTES) {
119
+ lineParts = [];
120
+ lineBytes = 0;
121
+ skippingOversizedLine = true;
122
+ return;
123
+ }
124
+ lineParts.push(segment);
125
+ lineBytes += segment.length;
126
+ };
127
+ while (readCursor < scanEnd) {
128
+ const toRead = Math.min(CONTROL_EVENT_READ_CHUNK_BYTES, scanEnd - readCursor);
129
+ const buffer = Buffer.alloc(toRead);
130
+ const bytesRead = fs.readSync(fd, buffer, 0, toRead, readCursor);
131
+ if (bytesRead <= 0) break;
132
+ const chunk = bytesRead === buffer.length ? buffer : buffer.subarray(0, bytesRead);
133
+ let lineStart = 0;
134
+ for (let index = 0; index < chunk.length; index++) {
135
+ if (chunk[index] !== 0x0a) continue;
136
+ appendLineSegment(chunk.subarray(lineStart, index));
137
+ if (!skippingOversizedLine && lineBytes > 0) {
138
+ handleLine(Buffer.concat(lineParts, lineBytes).toString("utf-8"));
139
+ }
140
+ lineParts = [];
141
+ lineBytes = 0;
142
+ skippingOversizedLine = false;
143
+ lastCompleteCursor = readCursor + index + 1;
144
+ lineStart = index + 1;
145
+ }
146
+ appendLineSegment(chunk.subarray(lineStart));
147
+ readCursor += bytesRead;
148
+ if (skippingOversizedLine) job.controlEventCursor = readCursor;
149
+ }
150
+ if (lastCompleteCursor > cursor) job.controlEventCursor = lastCompleteCursor;
151
+ else if (scanEnd < stat.size || startedFromTail) job.controlEventCursor = scanEnd;
152
+ } catch (error) {
153
+ console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
154
+ } finally {
155
+ fs.closeSync(fd);
156
+ }
157
+ };
158
+
159
+ const ensurePoller = () => {
160
+ if (state.poller) return;
161
+ state.poller = setInterval(() => {
162
+ if (state.asyncJobs.size === 0) {
163
+ if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext, []);
164
+ if (state.poller) {
165
+ clearInterval(state.poller);
166
+ state.poller = null;
167
+ }
168
+ return;
169
+ }
170
+
171
+ let widgetChanged = false;
172
+ for (const job of state.asyncJobs.values()) {
173
+ const widgetStateBefore = widgetRenderKey(job);
174
+ let nestedRefreshFailed = false;
175
+ const refreshNestedProjection = () => {
176
+ try {
177
+ updateAsyncJobNestedProjection(job);
178
+ } catch (error) {
179
+ nestedRefreshFailed = true;
180
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
181
+ }
182
+ };
183
+ const reconcileNestedDescendants = () => {
184
+ try {
185
+ if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
186
+ } catch (error) {
187
+ nestedRefreshFailed = true;
188
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
189
+ }
190
+ refreshNestedProjection();
191
+ };
192
+ try {
193
+ emitNewControlEvents(job);
194
+ reconcileNestedDescendants();
195
+ const reconciliation = reconcileAsyncRun(job.asyncDir, {
196
+ resultsDir,
197
+ kill: options.kill,
198
+ now: options.now,
199
+ startedRun: {
200
+ runId: job.asyncId,
201
+ pid: job.pid,
202
+ sessionId: job.sessionId,
203
+ mode: job.mode,
204
+ agents: job.agents,
205
+ chainStepCount: job.chainStepCount,
206
+ parallelGroups: job.parallelGroups,
207
+ startedAt: job.startedAt,
208
+ sessionFile: job.sessionFile,
209
+ },
210
+ });
211
+ const status = reconciliation.status ?? readStatus(job.asyncDir);
212
+ if (status) {
213
+ const previousStatus = job.status;
214
+ job.status = status.state;
215
+ if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
216
+ job.sessionId = status.sessionId ?? job.sessionId;
217
+ job.activityState = status.activityState;
218
+ job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
219
+ job.currentTool = status.currentTool;
220
+ job.currentToolStartedAt = status.currentToolStartedAt;
221
+ job.currentPath = status.currentPath;
222
+ job.turnCount = status.turnCount ?? job.turnCount;
223
+ job.toolCount = status.toolCount ?? job.toolCount;
224
+ job.mode = status.mode;
225
+ job.currentStep = status.currentStep ?? job.currentStep;
226
+ job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
227
+ job.startedAt = status.startedAt ?? job.startedAt;
228
+ if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
229
+ if (status.steps?.length) {
230
+ const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
231
+ job.parallelGroups = groups.length ? groups : job.parallelGroups;
232
+ job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
233
+ const activeGroup = status.currentStep !== undefined
234
+ ? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
235
+ : undefined;
236
+ const visibleSteps = activeGroup
237
+ ? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
238
+ : status.steps.map((step, index) => ({ ...step, index }));
239
+ job.activeParallelGroup = Boolean(activeGroup);
240
+ job.agents = visibleSteps.map((step) => step.agent);
241
+ job.steps = visibleSteps;
242
+ refreshNestedProjection();
243
+ job.stepsTotal = visibleSteps.length;
244
+ job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
245
+ job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
246
+ if (status.state === "complete") job.completedSteps = visibleSteps.length;
247
+ }
248
+ job.sessionDir = status.sessionDir ?? job.sessionDir;
249
+ job.outputFile = status.outputFile ?? job.outputFile;
250
+ job.totalTokens = status.totalTokens ?? job.totalTokens;
251
+ job.sessionFile = status.sessionFile ?? job.sessionFile;
252
+ if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
253
+ scheduleCleanup(job.asyncId);
254
+ }
255
+ if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
256
+ continue;
257
+ }
258
+ if (job.status === "queued") {
259
+ job.status = "running";
260
+ job.updatedAt = Date.now();
261
+ }
262
+ } catch (error) {
263
+ if (job.status !== "failed") {
264
+ console.error(`Failed to read async status for '${job.asyncDir}':`, error);
265
+ job.status = "failed";
266
+ job.updatedAt = Date.now();
267
+ }
268
+ if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
269
+ scheduleCleanup(job.asyncId);
270
+ }
271
+ }
272
+ if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
273
+ }
274
+
275
+ if (widgetChanged && state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
276
+ }, pollIntervalMs);
277
+ state.poller.unref?.();
278
+ };
279
+
280
+ const handleStarted = (data: unknown) => {
281
+ const info = data as AsyncStartedEvent;
282
+ if (!info.id) return;
283
+ const now = Date.now();
284
+ const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
285
+ const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
286
+ const validParallelGroups = normalizeParallelGroups(info.parallelGroups, Number.MAX_SAFE_INTEGER, info.chainStepCount ?? Number.MAX_SAFE_INTEGER);
287
+ const firstGroup = validParallelGroups.find((group) => group.start === 0);
288
+ const firstGroupCount = firstGroup?.count;
289
+ const agents = firstGroupCount && firstGroupCount > 0
290
+ ? rawAgents?.slice(0, firstGroupCount)
291
+ : rawAgents;
292
+ state.asyncJobs.set(info.id, {
293
+ asyncId: info.id,
294
+ asyncDir,
295
+ status: "queued",
296
+ pid: typeof info.pid === "number" ? info.pid : undefined,
297
+ ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
298
+ mode: info.mode ?? (info.chain ? "chain" : "single"),
299
+ agents,
300
+ chainStepCount: info.chainStepCount,
301
+ parallelGroups: validParallelGroups,
302
+ nestedRoute: info.nestedRoute,
303
+ stepsTotal: firstGroupCount ?? agents?.length,
304
+ hasParallelGroups: validParallelGroups.length > 0,
305
+ activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
306
+ startedAt: now,
307
+ updatedAt: now,
308
+ controlEventCursor: 0,
309
+ });
310
+ ensurePoller();
311
+ if (state.lastUiContext) {
312
+ rerenderWidget(state.lastUiContext);
313
+ }
314
+ };
315
+
316
+ const handleComplete = (data: unknown) => {
317
+ const result = data as { id?: string; success?: boolean; asyncDir?: string };
318
+ const asyncId = result.id;
319
+ if (!asyncId) return;
320
+ const job = state.asyncJobs.get(asyncId);
321
+ let nestedRefreshFailed = false;
322
+ if (job) {
323
+ job.status = result.success ? "complete" : "failed";
324
+ job.updatedAt = Date.now();
325
+ if (result.asyncDir) job.asyncDir = result.asyncDir;
326
+ try {
327
+ updateAsyncJobNestedProjection(job);
328
+ } catch (error) {
329
+ nestedRefreshFailed = true;
330
+ console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
331
+ }
332
+ }
333
+ if (state.lastUiContext) {
334
+ rerenderWidget(state.lastUiContext);
335
+ }
336
+ if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
337
+ };
338
+
339
+ const resetJobs = (ctx?: ExtensionContext) => {
340
+ for (const timer of state.cleanupTimers.values()) {
341
+ clearTimeout(timer);
342
+ }
343
+ state.cleanupTimers.clear();
344
+ state.asyncJobs.clear();
345
+ state.foregroundControls?.clear();
346
+ state.lastForegroundControlId = null;
347
+ state.resultFileCoalescer.clear();
348
+ if (ctx?.hasUI) {
349
+ state.lastUiContext = ctx;
350
+ rerenderWidget(ctx, []);
351
+ }
352
+ };
353
+
354
+ return { ensurePoller, handleStarted, handleComplete, resetJobs };
355
+ }