pi-subagents 0.21.4 → 0.21.5
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.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/package.json +3 -2
- package/src/extension/index.ts +4 -1
- package/src/intercom/result-intercom.ts +4 -3
- package/src/runs/background/async-execution.ts +16 -10
- package/src/runs/background/async-job-tracker.ts +12 -19
- package/src/runs/background/async-resume.ts +1 -0
- package/src/runs/background/async-status.ts +35 -49
- package/src/runs/background/parallel-groups.ts +45 -0
- package/src/runs/background/result-watcher.ts +2 -2
- package/src/runs/background/run-status.ts +26 -7
- package/src/runs/background/stale-run-reconciler.ts +15 -2
- package/src/runs/background/subagent-runner.ts +5 -3
- package/src/runs/foreground/subagent-executor.ts +6 -4
- package/src/shared/session-identity.ts +10 -0
- package/src/shared/types.ts +13 -5
- package/src/slash/slash-commands.ts +5 -1
- package/src/tui/render.ts +67 -2
- package/src/tui/subagents-status.ts +129 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.21.5] - 2026-05-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Show top-level async parallel runs as `parallel` instead of `chain`, with foreground-style running/done wording in widgets and status output, and group running async chain detail by chain step.
|
|
9
|
+
- Scoped `/subagents-status` to async runs launched from the current pi session instead of showing prior or unrelated sessions.
|
|
10
|
+
- Declared the Pi TUI package as a direct dev dependency and added a manifest guard so CI installs do not rely on transitive optional peer dependencies for tests.
|
|
11
|
+
- Made prompt-runtime extension path assertions portable on Windows.
|
|
12
|
+
|
|
5
13
|
## [0.21.4] - 2026-05-01
|
|
6
14
|
|
|
7
15
|
### Added
|
package/README.md
CHANGED
|
@@ -150,12 +150,14 @@ Use `~/.pi/agent/settings.json` for a user override or `.pi/settings.json` for a
|
|
|
150
150
|
|
|
151
151
|
Foreground runs stream progress in the conversation while they run.
|
|
152
152
|
|
|
153
|
-
Background runs keep working after control returns to you. They show completion notifications and can be inspected with:
|
|
153
|
+
Background runs keep working after control returns to you. They show a compact async widget, send completion notifications, and can be inspected with:
|
|
154
154
|
|
|
155
155
|
```text
|
|
156
156
|
/subagents-status
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
The status view shows active and recent runs for the current Pi session. Parallel background runs are shown as parallel work, with per-agent progress instead of fake chain steps. Chains with parallel groups keep their grouped shape in both progress and results views, so failed or paused agents stay visible next to completed ones.
|
|
160
|
+
|
|
159
161
|
You can also ask naturally:
|
|
160
162
|
|
|
161
163
|
```text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.5",
|
|
4
4
|
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@mariozechner/pi-agent-core": "^0.65.0",
|
|
79
79
|
"@mariozechner/pi-ai": "^0.65.0",
|
|
80
|
-
"@mariozechner/pi-coding-agent": "^0.65.0"
|
|
80
|
+
"@mariozechner/pi-coding-agent": "^0.65.0",
|
|
81
|
+
"@mariozechner/pi-tui": "^0.65.2"
|
|
81
82
|
}
|
|
82
83
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@
|
|
|
20
20
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@mariozechner/pi-tui";
|
|
21
21
|
import { discoverAgents } from "../agents/agents.ts";
|
|
22
22
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
23
|
+
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
23
24
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
24
25
|
import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "../tui/render.ts";
|
|
25
26
|
import { SubagentParams } from "./schemas.ts";
|
|
@@ -271,6 +272,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
271
272
|
const runtimeCleanup = () => {
|
|
272
273
|
stopWidgetAnimation();
|
|
273
274
|
stopResultAnimations();
|
|
275
|
+
stopResultWatcher();
|
|
274
276
|
clearPendingForegroundControlNotices(state);
|
|
275
277
|
if (state.poller) {
|
|
276
278
|
clearInterval(state.poller);
|
|
@@ -523,12 +525,13 @@ DIAGNOSTICS:
|
|
|
523
525
|
|
|
524
526
|
const resetSessionState = (ctx: ExtensionContext) => {
|
|
525
527
|
state.baseCwd = ctx.cwd;
|
|
526
|
-
state.currentSessionId = ctx.sessionManager
|
|
528
|
+
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
527
529
|
state.lastUiContext = ctx;
|
|
528
530
|
cleanupSessionArtifacts(ctx);
|
|
529
531
|
clearPendingForegroundControlNotices(state);
|
|
530
532
|
resetJobs(ctx);
|
|
531
533
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
534
|
+
primeExistingResults();
|
|
532
535
|
};
|
|
533
536
|
|
|
534
537
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type SubagentResultIntercomChild,
|
|
7
7
|
type SubagentResultIntercomPayload,
|
|
8
8
|
type SubagentResultStatus,
|
|
9
|
+
type SubagentRunMode,
|
|
9
10
|
SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
|
|
10
11
|
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
11
12
|
} from "../shared/types.ts";
|
|
@@ -61,7 +62,7 @@ function resolveGroupedStatus(children: SubagentResultIntercomChild[]): Subagent
|
|
|
61
62
|
interface GroupedResultIntercomMessageInput {
|
|
62
63
|
to: string;
|
|
63
64
|
runId: string;
|
|
64
|
-
mode:
|
|
65
|
+
mode: SubagentRunMode;
|
|
65
66
|
source: "foreground" | "async";
|
|
66
67
|
children: SubagentResultIntercomChild[];
|
|
67
68
|
asyncId?: string;
|
|
@@ -84,7 +85,7 @@ function asyncResumeGuidance(input: {
|
|
|
84
85
|
|
|
85
86
|
function formatSubagentResultIntercomMessage(input: {
|
|
86
87
|
runId: string;
|
|
87
|
-
mode:
|
|
88
|
+
mode: SubagentRunMode;
|
|
88
89
|
status: SubagentResultStatus;
|
|
89
90
|
source: "foreground" | "async";
|
|
90
91
|
children: SubagentResultIntercomChild[];
|
|
@@ -218,7 +219,7 @@ export function stripDetailsOutputsForIntercomReceipt(details: Details): Details
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
export function formatSubagentResultReceipt(input: {
|
|
221
|
-
mode:
|
|
222
|
+
mode: SubagentRunMode;
|
|
222
223
|
runId: string;
|
|
223
224
|
payload: SubagentResultIntercomPayload;
|
|
224
225
|
}): string {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type Details,
|
|
25
25
|
type MaxOutputConfig,
|
|
26
26
|
type ResolvedControlConfig,
|
|
27
|
+
type SubagentRunMode,
|
|
27
28
|
ASYNC_DIR,
|
|
28
29
|
RESULTS_DIR,
|
|
29
30
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
@@ -64,7 +65,7 @@ interface AsyncExecutionContext {
|
|
|
64
65
|
|
|
65
66
|
interface AsyncChainParams {
|
|
66
67
|
chain: ChainStep[];
|
|
67
|
-
resultMode?:
|
|
68
|
+
resultMode?: Exclude<SubagentRunMode, "single">;
|
|
68
69
|
agents: AgentConfig[];
|
|
69
70
|
ctx: AsyncExecutionContext;
|
|
70
71
|
availableModels?: AvailableModelInfo[];
|
|
@@ -160,7 +161,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
|
|
|
160
161
|
return { pid: proc.pid };
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function formatAsyncStartError(mode:
|
|
164
|
+
function formatAsyncStartError(mode: SubagentRunMode, message: string): AsyncExecutionResult {
|
|
164
165
|
return {
|
|
165
166
|
content: [{ type: "text", text: message }],
|
|
166
167
|
isError: true,
|
|
@@ -198,6 +199,7 @@ export function executeAsyncChain(
|
|
|
198
199
|
controlIntercomTarget,
|
|
199
200
|
childIntercomTarget,
|
|
200
201
|
} = params;
|
|
202
|
+
const resultMode = params.resultMode ?? "chain";
|
|
201
203
|
const chainSkills = params.chainSkills ?? [];
|
|
202
204
|
const availableModels = params.availableModels;
|
|
203
205
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
@@ -211,7 +213,7 @@ export function executeAsyncChain(
|
|
|
211
213
|
return {
|
|
212
214
|
content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
|
|
213
215
|
isError: true,
|
|
214
|
-
details: { mode:
|
|
216
|
+
details: { mode: resultMode, results: [] },
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
219
|
}
|
|
@@ -225,7 +227,7 @@ export function executeAsyncChain(
|
|
|
225
227
|
return {
|
|
226
228
|
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
227
229
|
isError: true,
|
|
228
|
-
details: { mode:
|
|
230
|
+
details: { mode: resultMode, results: [] },
|
|
229
231
|
};
|
|
230
232
|
}
|
|
231
233
|
|
|
@@ -329,7 +331,7 @@ export function executeAsyncChain(
|
|
|
329
331
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
330
332
|
});
|
|
331
333
|
} catch (error) {
|
|
332
|
-
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(
|
|
334
|
+
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(resultMode, error.message);
|
|
333
335
|
throw error;
|
|
334
336
|
}
|
|
335
337
|
let childTargetIndex = 0;
|
|
@@ -363,18 +365,18 @@ export function executeAsyncChain(
|
|
|
363
365
|
controlConfig,
|
|
364
366
|
controlIntercomTarget,
|
|
365
367
|
childIntercomTargets,
|
|
366
|
-
resultMode
|
|
368
|
+
resultMode,
|
|
367
369
|
},
|
|
368
370
|
id,
|
|
369
371
|
runnerCwd,
|
|
370
372
|
);
|
|
371
373
|
} catch (error) {
|
|
372
374
|
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
-
return formatAsyncStartError(
|
|
375
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${message}`);
|
|
374
376
|
}
|
|
375
377
|
|
|
376
378
|
if (spawnResult.error) {
|
|
377
|
-
return formatAsyncStartError(
|
|
379
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
if (spawnResult.pid) {
|
|
@@ -399,6 +401,8 @@ export function executeAsyncChain(
|
|
|
399
401
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
400
402
|
id,
|
|
401
403
|
pid: spawnResult.pid,
|
|
404
|
+
sessionId: ctx.currentSessionId,
|
|
405
|
+
mode: resultMode,
|
|
402
406
|
agent: firstAgents[0],
|
|
403
407
|
agents: flatAgents,
|
|
404
408
|
task: isParallelStep(firstStep)
|
|
@@ -421,8 +425,8 @@ export function executeAsyncChain(
|
|
|
421
425
|
.join(" -> ");
|
|
422
426
|
|
|
423
427
|
return {
|
|
424
|
-
content: [{ type: "text", text: `Async
|
|
425
|
-
details: { mode:
|
|
428
|
+
content: [{ type: "text", text: `Async ${resultMode}: ${chainDesc} [${id}]` }],
|
|
429
|
+
details: { mode: resultMode, results: [], asyncId: id, asyncDir },
|
|
426
430
|
};
|
|
427
431
|
}
|
|
428
432
|
|
|
@@ -543,6 +547,8 @@ export function executeAsyncSingle(
|
|
|
543
547
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
544
548
|
id,
|
|
545
549
|
pid: spawnResult.pid,
|
|
550
|
+
sessionId: ctx.currentSessionId,
|
|
551
|
+
mode: "single",
|
|
546
552
|
agent,
|
|
547
553
|
task: task?.slice(0, 50),
|
|
548
554
|
cwd: runnerCwd,
|
|
@@ -5,7 +5,6 @@ import { renderWidget } from "../../tui/render.ts";
|
|
|
5
5
|
import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
|
|
6
6
|
import {
|
|
7
7
|
type AsyncJobState,
|
|
8
|
-
type AsyncParallelGroupStatus,
|
|
9
8
|
type AsyncStartedEvent,
|
|
10
9
|
type ControlEvent,
|
|
11
10
|
type SubagentState,
|
|
@@ -15,25 +14,9 @@ import {
|
|
|
15
14
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
16
15
|
} from "../../shared/types.ts";
|
|
17
16
|
import { readStatus } from "../../shared/utils.ts";
|
|
17
|
+
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
18
18
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
|
|
22
|
-
return Number.isInteger(group.start)
|
|
23
|
-
&& Number.isInteger(group.count)
|
|
24
|
-
&& Number.isInteger(group.stepIndex)
|
|
25
|
-
&& group.start >= 0
|
|
26
|
-
&& group.count > 0
|
|
27
|
-
&& group.stepIndex >= 0
|
|
28
|
-
&& group.stepIndex < chainStepCount
|
|
29
|
-
&& group.start + group.count <= stepCount;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
33
|
-
if (!groups?.length) return [];
|
|
34
|
-
return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
20
|
interface AsyncJobTrackerOptions {
|
|
38
21
|
completionRetentionMs?: number;
|
|
39
22
|
pollIntervalMs?: number;
|
|
@@ -145,8 +128,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
145
128
|
startedRun: {
|
|
146
129
|
runId: job.asyncId,
|
|
147
130
|
pid: job.pid,
|
|
131
|
+
sessionId: job.sessionId,
|
|
148
132
|
mode: job.mode,
|
|
149
133
|
agents: job.agents,
|
|
134
|
+
chainStepCount: job.chainStepCount,
|
|
135
|
+
parallelGroups: job.parallelGroups,
|
|
150
136
|
startedAt: job.startedAt,
|
|
151
137
|
sessionFile: job.sessionFile,
|
|
152
138
|
},
|
|
@@ -155,6 +141,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
155
141
|
if (status) {
|
|
156
142
|
const previousStatus = job.status;
|
|
157
143
|
job.status = status.state;
|
|
144
|
+
job.sessionId = status.sessionId ?? job.sessionId;
|
|
158
145
|
job.activityState = status.activityState;
|
|
159
146
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
160
147
|
job.currentTool = status.currentTool;
|
|
@@ -164,10 +151,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
164
151
|
job.toolCount = status.toolCount ?? job.toolCount;
|
|
165
152
|
job.mode = status.mode;
|
|
166
153
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
154
|
+
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
167
155
|
job.startedAt = status.startedAt ?? job.startedAt;
|
|
168
156
|
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
169
157
|
if (status.steps?.length) {
|
|
170
158
|
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
159
|
+
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
171
160
|
job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
|
|
172
161
|
const activeGroup = status.currentStep !== undefined
|
|
173
162
|
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
@@ -177,6 +166,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
177
166
|
: status.steps;
|
|
178
167
|
job.activeParallelGroup = Boolean(activeGroup);
|
|
179
168
|
job.agents = visibleSteps.map((step) => step.agent);
|
|
169
|
+
job.steps = visibleSteps;
|
|
180
170
|
job.stepsTotal = visibleSteps.length;
|
|
181
171
|
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
182
172
|
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
@@ -225,8 +215,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
225
215
|
asyncDir,
|
|
226
216
|
status: "queued",
|
|
227
217
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
228
|
-
|
|
218
|
+
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
219
|
+
mode: info.mode ?? (info.chain ? "chain" : "single"),
|
|
229
220
|
agents,
|
|
221
|
+
chainStepCount: info.chainStepCount,
|
|
222
|
+
parallelGroups: validParallelGroups,
|
|
230
223
|
stepsTotal: firstGroupCount ?? agents?.length,
|
|
231
224
|
hasParallelGroups: validParallelGroups.length > 0,
|
|
232
225
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
@@ -202,6 +202,7 @@ function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
|
202
202
|
function validateStatusForResume(status: AsyncStatus | null, source: string): void {
|
|
203
203
|
if (!status) return;
|
|
204
204
|
if (typeof status.runId !== "string") throw new Error(`Invalid async status '${source}': runId must be a string.`);
|
|
205
|
+
if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
|
|
205
206
|
if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
|
|
206
207
|
if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
|
|
207
208
|
if (status.steps !== undefined) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
|
-
import { type ActivityState, type AsyncParallelGroupStatus, type AsyncStatus, type TokenUsage } from "../../shared/types.ts";
|
|
4
|
+
import { type ActivityState, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
|
+
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
6
7
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
7
8
|
|
|
8
9
|
interface AsyncRunStepSummary {
|
|
@@ -27,6 +28,7 @@ interface AsyncRunStepSummary {
|
|
|
27
28
|
export interface AsyncRunSummary {
|
|
28
29
|
id: string;
|
|
29
30
|
asyncDir: string;
|
|
31
|
+
sessionId?: string;
|
|
30
32
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
31
33
|
activityState?: ActivityState;
|
|
32
34
|
lastActivityAt?: number;
|
|
@@ -35,7 +37,7 @@ export interface AsyncRunSummary {
|
|
|
35
37
|
currentPath?: string;
|
|
36
38
|
turnCount?: number;
|
|
37
39
|
toolCount?: number;
|
|
38
|
-
mode:
|
|
40
|
+
mode: SubagentRunMode;
|
|
39
41
|
cwd?: string;
|
|
40
42
|
startedAt: number;
|
|
41
43
|
lastUpdate?: number;
|
|
@@ -50,45 +52,9 @@ export interface AsyncRunSummary {
|
|
|
50
52
|
sessionFile?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
|
|
54
|
-
return Number.isInteger(group.start)
|
|
55
|
-
&& Number.isInteger(group.count)
|
|
56
|
-
&& Number.isInteger(group.stepIndex)
|
|
57
|
-
&& group.start >= 0
|
|
58
|
-
&& group.count > 0
|
|
59
|
-
&& group.stepIndex >= 0
|
|
60
|
-
&& group.stepIndex < chainStepCount
|
|
61
|
-
&& group.start + group.count <= stepCount;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
65
|
-
if (!groups?.length) return [];
|
|
66
|
-
return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function flatToLogicalStepIndex(flatIndex: number, chainStepCount: number, parallelGroups: AsyncParallelGroupStatus[]): number {
|
|
70
|
-
let logicalIndex = 0;
|
|
71
|
-
let cursor = 0;
|
|
72
|
-
for (const group of parallelGroups) {
|
|
73
|
-
while (logicalIndex < chainStepCount && cursor < group.start) {
|
|
74
|
-
if (flatIndex === cursor) return logicalIndex;
|
|
75
|
-
logicalIndex++;
|
|
76
|
-
cursor++;
|
|
77
|
-
}
|
|
78
|
-
if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
|
|
79
|
-
logicalIndex = Math.max(logicalIndex, group.stepIndex + 1);
|
|
80
|
-
cursor = group.start + group.count;
|
|
81
|
-
}
|
|
82
|
-
while (logicalIndex < chainStepCount) {
|
|
83
|
-
if (flatIndex === cursor) return logicalIndex;
|
|
84
|
-
logicalIndex++;
|
|
85
|
-
cursor++;
|
|
86
|
-
}
|
|
87
|
-
return Math.max(0, chainStepCount - 1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
55
|
interface AsyncRunListOptions {
|
|
91
56
|
states?: Array<AsyncRunSummary["state"]>;
|
|
57
|
+
sessionId?: string;
|
|
92
58
|
limit?: number;
|
|
93
59
|
resultsDir?: string;
|
|
94
60
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
@@ -101,6 +67,11 @@ export interface AsyncRunOverlayData {
|
|
|
101
67
|
recent: AsyncRunSummary[];
|
|
102
68
|
}
|
|
103
69
|
|
|
70
|
+
export interface AsyncRunOverlayOptions {
|
|
71
|
+
recentLimit?: number;
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
104
75
|
function getErrorMessage(error: unknown): string {
|
|
105
76
|
return error instanceof Error ? error.message : String(error);
|
|
106
77
|
}
|
|
@@ -147,6 +118,9 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
147
118
|
}
|
|
148
119
|
|
|
149
120
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
121
|
+
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
122
|
+
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
123
|
+
}
|
|
150
124
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
151
125
|
const steps = status.steps ?? [];
|
|
152
126
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
@@ -154,6 +128,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
154
128
|
return {
|
|
155
129
|
id: status.runId || path.basename(asyncDir),
|
|
156
130
|
asyncDir,
|
|
131
|
+
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
157
132
|
state: status.state,
|
|
158
133
|
activityState,
|
|
159
134
|
lastActivityAt,
|
|
@@ -240,6 +215,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
240
215
|
if (!status) continue;
|
|
241
216
|
const summary = statusToSummary(asyncDir, status);
|
|
242
217
|
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
218
|
+
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
243
219
|
runs.push(summary);
|
|
244
220
|
}
|
|
245
221
|
|
|
@@ -247,8 +223,9 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
247
223
|
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
248
224
|
}
|
|
249
225
|
|
|
250
|
-
export function listAsyncRunsForOverlay(asyncDirRoot: string,
|
|
251
|
-
const
|
|
226
|
+
export function listAsyncRunsForOverlay(asyncDirRoot: string, options: AsyncRunOverlayOptions = {}): AsyncRunOverlayData {
|
|
227
|
+
const recentLimit = options.recentLimit ?? 5;
|
|
228
|
+
const all = listAsyncRuns(asyncDirRoot, { sessionId: options.sessionId });
|
|
252
229
|
const recent = all
|
|
253
230
|
.filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
|
|
254
231
|
.sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
|
|
@@ -287,6 +264,18 @@ function formatStepLine(step: AsyncRunStepSummary): string {
|
|
|
287
264
|
return parts.join(" | ");
|
|
288
265
|
}
|
|
289
266
|
|
|
267
|
+
function formatParallelProgress(steps: Pick<AsyncRunStepSummary, "status">[], total: number, showRunning: boolean): string {
|
|
268
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
269
|
+
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
270
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
271
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
272
|
+
const parts = [`${done}/${total} done`];
|
|
273
|
+
if (showRunning) parts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
274
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
275
|
+
if (paused > 0) parts.push(`${paused} paused`);
|
|
276
|
+
return parts.join(" · ");
|
|
277
|
+
}
|
|
278
|
+
|
|
290
279
|
export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
|
|
291
280
|
const stepCount = run.steps.length || 1;
|
|
292
281
|
const chainStepCount = run.chainStepCount ?? stepCount;
|
|
@@ -294,16 +283,13 @@ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" |
|
|
|
294
283
|
const activeGroup = run.currentStep !== undefined
|
|
295
284
|
? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
|
|
296
285
|
: undefined;
|
|
297
|
-
if (
|
|
286
|
+
if (activeGroup) {
|
|
298
287
|
const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const groupLabel = run.state === "running"
|
|
303
|
-
? `parallel group: ${runningLabel} · ${done}/${activeGroup.count} done`
|
|
304
|
-
: `parallel group: ${done}/${activeGroup.count} done`;
|
|
305
|
-
return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · ${groupLabel}`;
|
|
288
|
+
const groupLabel = formatParallelProgress(groupSteps, activeGroup.count, run.state === "running");
|
|
289
|
+
if (run.mode === "parallel") return groupLabel;
|
|
290
|
+
return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · parallel group: ${groupLabel}`;
|
|
306
291
|
}
|
|
292
|
+
if (run.mode === "parallel") return formatParallelProgress(run.steps, stepCount, run.state === "running");
|
|
307
293
|
if (run.mode === "chain" && run.currentStep !== undefined && groups.length > 0) {
|
|
308
294
|
const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
|
|
309
295
|
return `step ${logicalStep + 1}/${chainStepCount}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AsyncParallelGroupStatus } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
function isValidParallelGroup(group: unknown, stepCount: number, chainStepCount: number): group is AsyncParallelGroupStatus {
|
|
4
|
+
if (typeof group !== "object" || group === null) return false;
|
|
5
|
+
const { start, count, stepIndex } = group as Partial<AsyncParallelGroupStatus>;
|
|
6
|
+
return typeof start === "number"
|
|
7
|
+
&& typeof count === "number"
|
|
8
|
+
&& typeof stepIndex === "number"
|
|
9
|
+
&& Number.isInteger(start)
|
|
10
|
+
&& Number.isInteger(count)
|
|
11
|
+
&& Number.isInteger(stepIndex)
|
|
12
|
+
&& start >= 0
|
|
13
|
+
&& count > 0
|
|
14
|
+
&& stepIndex >= 0
|
|
15
|
+
&& stepIndex < chainStepCount
|
|
16
|
+
&& start + count <= stepCount;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeParallelGroups(groups: unknown, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
20
|
+
if (!Array.isArray(groups)) return [];
|
|
21
|
+
return groups
|
|
22
|
+
.filter((group): group is AsyncParallelGroupStatus => isValidParallelGroup(group, stepCount, chainStepCount))
|
|
23
|
+
.sort((left, right) => left.stepIndex - right.stepIndex || left.start - right.start);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function flatToLogicalStepIndex(flatIndex: number, chainStepCount: number, groups: AsyncParallelGroupStatus[]): number {
|
|
27
|
+
let logicalIndex = 0;
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
while (cursor < group.start && logicalIndex < chainStepCount) {
|
|
31
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
32
|
+
cursor++;
|
|
33
|
+
logicalIndex++;
|
|
34
|
+
}
|
|
35
|
+
if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
|
|
36
|
+
cursor = group.start + group.count;
|
|
37
|
+
logicalIndex = group.stepIndex + 1;
|
|
38
|
+
}
|
|
39
|
+
while (cursor <= flatIndex && logicalIndex < chainStepCount) {
|
|
40
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
41
|
+
cursor++;
|
|
42
|
+
logicalIndex++;
|
|
43
|
+
}
|
|
44
|
+
return Math.max(0, chainStepCount - 1);
|
|
45
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
3
|
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
5
4
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
6
5
|
import {
|
|
7
6
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
7
|
+
type IntercomEventBus,
|
|
8
8
|
type SubagentState,
|
|
9
9
|
} from "../../shared/types.ts";
|
|
10
10
|
import {
|
|
@@ -46,7 +46,7 @@ function shouldFallBackToPolling(error: unknown): boolean {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function createResultWatcher(
|
|
49
|
-
pi:
|
|
49
|
+
pi: { events: IntercomEventBus },
|
|
50
50
|
state: SubagentState,
|
|
51
51
|
resultsDir: string,
|
|
52
52
|
completionTtlMs: number,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
4
|
-
import { formatAsyncRunList, listAsyncRuns } from "./async-status.ts";
|
|
5
|
-
import { ASYNC_DIR, RESULTS_DIR, type Details } from "../../shared/types.ts";
|
|
4
|
+
import { formatAsyncRunList, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
|
+
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
|
|
6
6
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
7
7
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
8
|
+
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
8
9
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
9
10
|
|
|
10
11
|
interface RunStatusParams {
|
|
@@ -31,6 +32,19 @@ function canShowRevive(stepCount: number, sessionFile: unknown): sessionFile is
|
|
|
31
32
|
return stepCount === 1 && typeof sessionFile === "string" && fs.existsSync(sessionFile);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
36
|
+
const steps = status.steps ?? [];
|
|
37
|
+
if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
|
|
38
|
+
if (status.mode === "chain") {
|
|
39
|
+
const chainStepCount = status.chainStepCount ?? (steps.length || 1);
|
|
40
|
+
const groups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
41
|
+
const group = groups.find((candidate) => index >= candidate.start && index < candidate.start + candidate.count);
|
|
42
|
+
if (group) return `Step ${group.stepIndex + 1}/${chainStepCount} Agent ${index - group.start + 1}/${group.count}`;
|
|
43
|
+
return `Step ${flatToLogicalStepIndex(index, chainStepCount, groups) + 1}/${chainStepCount}`;
|
|
44
|
+
}
|
|
45
|
+
return `Step ${index + 1}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
|
|
35
49
|
const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
|
|
36
50
|
const resultsDir = deps.resultsDir ?? RESULTS_DIR;
|
|
@@ -89,9 +103,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
89
103
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
90
104
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
91
105
|
if (status) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
const progressLabel = formatAsyncRunProgressLabel({
|
|
107
|
+
mode: status.mode,
|
|
108
|
+
state: status.state,
|
|
109
|
+
currentStep: status.currentStep,
|
|
110
|
+
chainStepCount: status.chainStepCount,
|
|
111
|
+
parallelGroups: status.parallelGroups,
|
|
112
|
+
steps: (status.steps ?? []).map((step, index) => ({ index, agent: step.agent, status: step.status })),
|
|
113
|
+
});
|
|
95
114
|
const started = new Date(status.startedAt).toISOString();
|
|
96
115
|
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
97
116
|
const statusActivityText = status.state === "running" ? activityText(status.activityState, status.lastActivityAt) : undefined;
|
|
@@ -101,7 +120,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
101
120
|
`State: ${status.state}`,
|
|
102
121
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
103
122
|
`Mode: ${status.mode}`,
|
|
104
|
-
|
|
123
|
+
`Progress: ${progressLabel}`,
|
|
105
124
|
`Started: ${started}`,
|
|
106
125
|
`Updated: ${updated}`,
|
|
107
126
|
`Dir: ${asyncDir}`,
|
|
@@ -111,7 +130,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
111
130
|
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
112
131
|
const stepActivityText = step.status === "running" ? activityText(step.activityState, step.lastActivityAt) : undefined;
|
|
113
132
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
114
|
-
lines.push(
|
|
133
|
+
lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
|
|
115
134
|
if (step.status === "running") {
|
|
116
135
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
117
136
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
-
import { RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
|
|
4
|
+
import { RESULTS_DIR, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode } from "../../shared/types.ts";
|
|
5
|
+
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
5
6
|
|
|
6
7
|
export type PidLiveness = "alive" | "dead" | "unknown";
|
|
7
8
|
|
|
@@ -10,8 +11,11 @@ type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
|
10
11
|
interface StartedRunMetadata {
|
|
11
12
|
runId: string;
|
|
12
13
|
pid?: number;
|
|
13
|
-
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
mode?: SubagentRunMode;
|
|
14
16
|
agents?: string[];
|
|
17
|
+
chainStepCount?: number;
|
|
18
|
+
parallelGroups?: AsyncParallelGroupStatus[];
|
|
15
19
|
startedAt?: number;
|
|
16
20
|
sessionFile?: string;
|
|
17
21
|
}
|
|
@@ -133,13 +137,21 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
133
137
|
function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, now: number): AsyncStatus {
|
|
134
138
|
const startedAt = startedRun.startedAt ?? now;
|
|
135
139
|
const agents = startedRun.agents?.length ? startedRun.agents : ["subagent"];
|
|
140
|
+
const chainStepCount = startedRun.chainStepCount;
|
|
141
|
+
const parallelGroups = chainStepCount !== undefined
|
|
142
|
+
? normalizeParallelGroups(startedRun.parallelGroups, agents.length, chainStepCount)
|
|
143
|
+
: [];
|
|
136
144
|
return {
|
|
137
145
|
runId: startedRun.runId || path.basename(asyncDir),
|
|
146
|
+
...(startedRun.sessionId ? { sessionId: startedRun.sessionId } : {}),
|
|
138
147
|
mode: startedRun.mode ?? "single",
|
|
139
148
|
state: "running",
|
|
140
149
|
pid: startedRun.pid,
|
|
141
150
|
startedAt,
|
|
142
151
|
lastUpdate: now,
|
|
152
|
+
currentStep: 0,
|
|
153
|
+
...(chainStepCount !== undefined ? { chainStepCount } : {}),
|
|
154
|
+
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
143
155
|
steps: agents.map((agent) => ({
|
|
144
156
|
agent,
|
|
145
157
|
status: "running" as const,
|
|
@@ -197,6 +209,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
197
209
|
timestamp: now,
|
|
198
210
|
durationMs: Math.max(0, now - status.startedAt),
|
|
199
211
|
asyncDir,
|
|
212
|
+
sessionId: status.sessionId,
|
|
200
213
|
sessionFile: status.sessionFile,
|
|
201
214
|
},
|
|
202
215
|
};
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type AsyncStatus,
|
|
17
17
|
type ModelAttempt,
|
|
18
18
|
type ResolvedControlConfig,
|
|
19
|
+
type SubagentRunMode,
|
|
19
20
|
type Usage,
|
|
20
21
|
DEFAULT_MAX_OUTPUT,
|
|
21
22
|
type MaxOutputConfig,
|
|
@@ -90,7 +91,7 @@ interface SubagentRunConfig {
|
|
|
90
91
|
controlConfig?: ResolvedControlConfig;
|
|
91
92
|
controlIntercomTarget?: string;
|
|
92
93
|
childIntercomTargets?: Array<string | undefined>;
|
|
93
|
-
resultMode?:
|
|
94
|
+
resultMode?: SubagentRunMode;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
interface StepResult {
|
|
@@ -472,7 +473,7 @@ function writeRunLog(
|
|
|
472
473
|
logPath: string,
|
|
473
474
|
input: {
|
|
474
475
|
id: string;
|
|
475
|
-
mode:
|
|
476
|
+
mode: SubagentRunMode;
|
|
476
477
|
cwd: string;
|
|
477
478
|
startedAt: number;
|
|
478
479
|
endedAt: number;
|
|
@@ -877,7 +878,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
877
878
|
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
878
879
|
const statusPayload: RunnerStatusPayload = {
|
|
879
880
|
runId: id,
|
|
880
|
-
|
|
881
|
+
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
882
|
+
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
881
883
|
state: "running",
|
|
882
884
|
lastActivityAt: overallStartTime,
|
|
883
885
|
startedAt: overallStartTime,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
30
30
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "../background/async-execution.ts";
|
|
31
31
|
import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
32
|
+
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
32
33
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
33
34
|
import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
|
|
34
35
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
type MaxOutputConfig,
|
|
66
67
|
type ResolvedControlConfig,
|
|
67
68
|
type SingleResult,
|
|
69
|
+
type SubagentRunMode,
|
|
68
70
|
type SubagentState,
|
|
69
71
|
DEFAULT_ARTIFACT_CONFIG,
|
|
70
72
|
SUBAGENT_ACTIONS,
|
|
@@ -332,7 +334,7 @@ async function resumeAsyncRun(input: {
|
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
|
|
335
|
-
input.deps.state.currentSessionId =
|
|
337
|
+
input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
|
|
336
338
|
const effectiveCwd = target.cwd ?? input.requestCwd;
|
|
337
339
|
const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
|
|
338
340
|
const discoveredAgents = input.deps.discoverAgents(effectiveCwd, scope).agents;
|
|
@@ -423,7 +425,7 @@ async function emitForegroundResultIntercom(input: {
|
|
|
423
425
|
pi: ExtensionAPI;
|
|
424
426
|
intercomBridge: IntercomBridgeState;
|
|
425
427
|
runId: string;
|
|
426
|
-
mode:
|
|
428
|
+
mode: SubagentRunMode;
|
|
427
429
|
results: SingleResult[];
|
|
428
430
|
chainSteps?: number;
|
|
429
431
|
}): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
|
|
@@ -459,7 +461,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
|
|
|
459
461
|
pi: ExtensionAPI;
|
|
460
462
|
intercomBridge: IntercomBridgeState;
|
|
461
463
|
runId: string;
|
|
462
|
-
mode:
|
|
464
|
+
mode: SubagentRunMode;
|
|
463
465
|
details: Details;
|
|
464
466
|
}): Promise<{ text: string; details: Details } | null> {
|
|
465
467
|
const payload = await emitForegroundResultIntercom({
|
|
@@ -1966,7 +1968,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1966
1968
|
const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
|
|
1967
1969
|
const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
|
|
1968
1970
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1969
|
-
deps.state.currentSessionId =
|
|
1971
|
+
deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
1970
1972
|
const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
|
|
1971
1973
|
effectiveParams = applyAgentDefaultContext(effectiveParams, discoveredAgents);
|
|
1972
1974
|
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface SessionIdentityManager {
|
|
2
|
+
getSessionFile(): string | null | undefined;
|
|
3
|
+
getSessionId(): string | null | undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function resolveCurrentSessionId(sessionManager: SessionIdentityManager): string {
|
|
7
|
+
const sessionId = sessionManager.getSessionFile() ?? sessionManager.getSessionId();
|
|
8
|
+
if (!sessionId) throw new Error("Current session identity is unavailable.");
|
|
9
|
+
return sessionId;
|
|
10
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface ControlEvent {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
99
|
+
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
99
100
|
|
|
100
101
|
export interface SubagentResultIntercomChild {
|
|
101
102
|
agent: string;
|
|
@@ -112,7 +113,7 @@ export interface SubagentResultIntercomPayload {
|
|
|
112
113
|
message: string;
|
|
113
114
|
requestId?: string;
|
|
114
115
|
runId: string;
|
|
115
|
-
mode:
|
|
116
|
+
mode: SubagentRunMode;
|
|
116
117
|
status: SubagentResultStatus;
|
|
117
118
|
summary: string;
|
|
118
119
|
source: "foreground" | "async";
|
|
@@ -205,7 +206,7 @@ export interface SingleResult {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
export interface Details {
|
|
208
|
-
mode:
|
|
209
|
+
mode: SubagentRunMode | "management";
|
|
209
210
|
context?: "fresh" | "fork";
|
|
210
211
|
results: SingleResult[];
|
|
211
212
|
controlEvents?: ControlEvent[];
|
|
@@ -263,6 +264,8 @@ export interface AsyncStartedEvent {
|
|
|
263
264
|
id?: string;
|
|
264
265
|
asyncDir?: string;
|
|
265
266
|
pid?: number;
|
|
267
|
+
sessionId?: string;
|
|
268
|
+
mode?: SubagentRunMode;
|
|
266
269
|
agent?: string;
|
|
267
270
|
agents?: string[];
|
|
268
271
|
chain?: string[];
|
|
@@ -272,7 +275,8 @@ export interface AsyncStartedEvent {
|
|
|
272
275
|
|
|
273
276
|
export interface AsyncStatus {
|
|
274
277
|
runId: string;
|
|
275
|
-
|
|
278
|
+
sessionId?: string;
|
|
279
|
+
mode: SubagentRunMode;
|
|
276
280
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
277
281
|
activityState?: ActivityState;
|
|
278
282
|
lastActivityAt?: number;
|
|
@@ -321,6 +325,7 @@ export interface AsyncJobState {
|
|
|
321
325
|
asyncDir: string;
|
|
322
326
|
status: "queued" | "running" | "complete" | "failed" | "paused";
|
|
323
327
|
pid?: number;
|
|
328
|
+
sessionId?: string;
|
|
324
329
|
activityState?: ActivityState;
|
|
325
330
|
lastActivityAt?: number;
|
|
326
331
|
currentTool?: string;
|
|
@@ -328,9 +333,12 @@ export interface AsyncJobState {
|
|
|
328
333
|
currentPath?: string;
|
|
329
334
|
turnCount?: number;
|
|
330
335
|
toolCount?: number;
|
|
331
|
-
mode?:
|
|
336
|
+
mode?: SubagentRunMode;
|
|
332
337
|
agents?: string[];
|
|
333
338
|
currentStep?: number;
|
|
339
|
+
chainStepCount?: number;
|
|
340
|
+
parallelGroups?: AsyncParallelGroupStatus[];
|
|
341
|
+
steps?: AsyncStatus["steps"];
|
|
334
342
|
stepsTotal?: number;
|
|
335
343
|
runningSteps?: number;
|
|
336
344
|
completedSteps?: number;
|
|
@@ -351,7 +359,7 @@ export interface SubagentState {
|
|
|
351
359
|
asyncJobs: Map<string, AsyncJobState>;
|
|
352
360
|
foregroundControls: Map<string, {
|
|
353
361
|
runId: string;
|
|
354
|
-
mode:
|
|
362
|
+
mode: SubagentRunMode;
|
|
355
363
|
startedAt: number;
|
|
356
364
|
updatedAt: number;
|
|
357
365
|
currentAgent?: string;
|
|
@@ -8,6 +8,7 @@ import { AgentManagerComponent, type ManagerResult } from "../manager-ui/agent-m
|
|
|
8
8
|
import { SubagentsStatusComponent } from "../tui/subagents-status.ts";
|
|
9
9
|
import { discoverAvailableSkills } from "../agents/skills.ts";
|
|
10
10
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
11
|
+
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
11
12
|
import { isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
12
13
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
13
14
|
import {
|
|
@@ -571,8 +572,11 @@ export function registerSlashCommands(
|
|
|
571
572
|
pi.registerCommand("subagents-status", {
|
|
572
573
|
description: "Show active and recent async subagent runs",
|
|
573
574
|
handler: async (_args, ctx) => {
|
|
575
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
576
|
+
state.baseCwd = ctx.cwd;
|
|
577
|
+
state.currentSessionId = sessionId;
|
|
574
578
|
await ctx.ui.custom<void>(
|
|
575
|
-
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined)),
|
|
579
|
+
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
|
|
576
580
|
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
577
581
|
);
|
|
578
582
|
},
|
package/src/tui/render.ts
CHANGED
|
@@ -256,7 +256,17 @@ function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
|
|
|
256
256
|
return jobs.some((job) => job.status === "running");
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
function formatWidgetAgents(agents: string[]): string {
|
|
260
|
+
const distinct = [...new Set(agents)];
|
|
261
|
+
if (distinct.length === 1 && agents.length > 1) return `${distinct[0]} ×${agents.length}`;
|
|
262
|
+
if (agents.length > 3) return `${agents.slice(0, 2).join(", ")} +${agents.length - 2} more`;
|
|
263
|
+
return agents.join(", ");
|
|
264
|
+
}
|
|
265
|
+
|
|
259
266
|
function widgetJobName(job: AsyncJobState): string {
|
|
267
|
+
const agents = job.agents?.length ? formatWidgetAgents(job.agents) : undefined;
|
|
268
|
+
if (job.mode === "parallel") return agents ? `parallel · ${agents}` : "parallel";
|
|
269
|
+
if (job.activeParallelGroup) return agents ? `parallel group · ${agents}` : "parallel group";
|
|
260
270
|
if (job.agents?.length) return job.agents.join(" → ");
|
|
261
271
|
return job.mode ?? "subagent";
|
|
262
272
|
}
|
|
@@ -297,6 +307,47 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
297
307
|
return theme.fg("error", "✗");
|
|
298
308
|
}
|
|
299
309
|
|
|
310
|
+
function widgetStepGlyph(status: string, theme: Theme): string {
|
|
311
|
+
if (status === "running") return theme.fg("accent", "▶");
|
|
312
|
+
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
313
|
+
if (status === "failed") return theme.fg("error", "✗");
|
|
314
|
+
if (status === "paused") return theme.fg("warning", "■");
|
|
315
|
+
return theme.fg("muted", "◦");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function widgetStepStatus(status: string, theme: Theme): string {
|
|
319
|
+
if (status === "running") return theme.fg("accent", "running");
|
|
320
|
+
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
321
|
+
if (status === "failed") return theme.fg("error", "failed");
|
|
322
|
+
if (status === "paused") return theme.fg("warning", "paused");
|
|
323
|
+
return theme.fg("dim", status);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]): string {
|
|
327
|
+
const facts: string[] = [];
|
|
328
|
+
if (step.currentTool && step.currentToolStartedAt !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, Date.now() - step.currentToolStartedAt))}`);
|
|
329
|
+
else if (step.currentTool) facts.push(step.currentTool);
|
|
330
|
+
if (step.currentPath) facts.push(shortenPath(step.currentPath));
|
|
331
|
+
if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
|
|
332
|
+
if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
|
|
333
|
+
if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
|
|
334
|
+
const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
|
|
335
|
+
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
336
|
+
if (activity) return activity;
|
|
337
|
+
return facts.join(" · ");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
|
|
341
|
+
if (!job.activeParallelGroup || !job.steps?.length) return [];
|
|
342
|
+
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
343
|
+
const total = job.stepsTotal ?? job.steps.length;
|
|
344
|
+
return job.steps.map((step, index) => {
|
|
345
|
+
const marker = index === job.steps!.length - 1 ? "└" : "├";
|
|
346
|
+
const activity = widgetStepActivity(step);
|
|
347
|
+
return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} Agent ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${activity ? ` · ${activity}` : ""}`)}`;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
300
351
|
function parseParallelGroupAgentCount(label: string | undefined): number | undefined {
|
|
301
352
|
if (!label || !label.startsWith("[") || !label.endsWith("]")) return undefined;
|
|
302
353
|
const inner = label.slice(1, -1).trim();
|
|
@@ -461,8 +512,20 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
461
512
|
if (job.activeParallelGroup) {
|
|
462
513
|
const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
|
|
463
514
|
const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
|
|
464
|
-
if (job.
|
|
465
|
-
|
|
515
|
+
if (job.mode === "parallel") {
|
|
516
|
+
if (job.status === "running") parts.push(formatAgentRunningLabel(running));
|
|
517
|
+
if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
|
|
518
|
+
} else {
|
|
519
|
+
const activeGroup = job.currentStep !== undefined
|
|
520
|
+
? job.parallelGroups?.find((group) => job.currentStep! >= group.start && job.currentStep! < group.start + group.count)
|
|
521
|
+
: job.parallelGroups?.find((group) => group.start === 0);
|
|
522
|
+
const logicalStep = activeGroup?.stepIndex ?? job.currentStep ?? 0;
|
|
523
|
+
const total = job.chainStepCount ?? stepsTotal;
|
|
524
|
+
const groupProgress = job.status === "running"
|
|
525
|
+
? `${formatAgentRunningLabel(running)} · ${done}/${stepsTotal} done`
|
|
526
|
+
: `${done}/${stepsTotal} done`;
|
|
527
|
+
parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupProgress}`);
|
|
528
|
+
}
|
|
466
529
|
} else if (job.currentStep !== undefined) {
|
|
467
530
|
parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
|
|
468
531
|
} else if (stepsTotal > 1) {
|
|
@@ -496,6 +559,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
496
559
|
items.push([
|
|
497
560
|
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
498
561
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
562
|
+
...widgetParallelAgentDetails(job, theme),
|
|
499
563
|
]);
|
|
500
564
|
slots--;
|
|
501
565
|
}
|
|
@@ -512,6 +576,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
512
576
|
items.push([
|
|
513
577
|
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
514
578
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
579
|
+
...widgetParallelAgentDetails(job, theme),
|
|
515
580
|
]);
|
|
516
581
|
slots--;
|
|
517
582
|
}
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
5
5
|
import { matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
6
|
-
import { type AsyncRunOverlayData, type AsyncRunSummary, formatAsyncRunProgressLabel, listAsyncRunsForOverlay } from "../runs/background/async-status.ts";
|
|
6
|
+
import { type AsyncRunOverlayData, type AsyncRunOverlayOptions, type AsyncRunSummary, formatAsyncRunProgressLabel, listAsyncRunsForOverlay } from "../runs/background/async-status.ts";
|
|
7
7
|
import { ASYNC_DIR } from "../shared/types.ts";
|
|
8
8
|
import { formatDuration, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
9
9
|
import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.ts";
|
|
@@ -20,8 +20,18 @@ interface StatusRow {
|
|
|
20
20
|
run?: AsyncRunSummary;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
type AsyncRunStep = AsyncRunSummary["steps"][number];
|
|
24
|
+
|
|
25
|
+
interface ChainStepSpan {
|
|
26
|
+
stepIndex: number;
|
|
27
|
+
start: number;
|
|
28
|
+
count: number;
|
|
29
|
+
isParallel: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
interface StatusOverlayDeps {
|
|
24
|
-
|
|
33
|
+
sessionId: string;
|
|
34
|
+
listRunsForOverlay?: (asyncDirRoot: string, options?: AsyncRunOverlayOptions) => AsyncRunOverlayData;
|
|
25
35
|
refreshMs?: number;
|
|
26
36
|
}
|
|
27
37
|
|
|
@@ -44,6 +54,14 @@ function stepStatusColor(theme: Theme, status: string): string {
|
|
|
44
54
|
return status;
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
function stepGlyph(theme: Theme, status: string): string {
|
|
58
|
+
if (status === "running") return theme.fg("accent", "▶");
|
|
59
|
+
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
60
|
+
if (status === "failed") return theme.fg("error", "✗");
|
|
61
|
+
if (status === "paused") return theme.fg("warning", "■");
|
|
62
|
+
return theme.fg("dim", "◦");
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
function runLabel(theme: Theme, run: AsyncRunSummary, selected: boolean): string {
|
|
48
66
|
const prefix = selected ? theme.fg("accent", ">") : " ";
|
|
49
67
|
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
@@ -76,6 +94,40 @@ function buildRows(active: AsyncRunSummary[], recent: AsyncRunSummary[]): Status
|
|
|
76
94
|
return rows;
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
function buildChainStepSpans(run: AsyncRunSummary): ChainStepSpan[] {
|
|
98
|
+
const total = run.chainStepCount ?? run.steps.length;
|
|
99
|
+
const groups = [...(run.parallelGroups ?? [])].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
100
|
+
const spans: ChainStepSpan[] = [];
|
|
101
|
+
let flatIndex = 0;
|
|
102
|
+
for (let stepIndex = 0; stepIndex < total; stepIndex++) {
|
|
103
|
+
const group = groups.find((candidate) => candidate.stepIndex === stepIndex);
|
|
104
|
+
if (group) {
|
|
105
|
+
spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
|
|
106
|
+
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
spans.push({ stepIndex, start: flatIndex, count: flatIndex < run.steps.length ? 1 : 0, isParallel: false });
|
|
110
|
+
flatIndex++;
|
|
111
|
+
}
|
|
112
|
+
return spans;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function aggregateStepStatus(steps: AsyncRunStep[]): string {
|
|
116
|
+
if (steps.some((step) => step.status === "running")) return "running";
|
|
117
|
+
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
118
|
+
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
119
|
+
if (steps.length > 0 && steps.every((step) => step.status === "complete" || step.status === "completed")) return "complete";
|
|
120
|
+
return "pending";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compactStepStats(step: AsyncRunStep): string {
|
|
124
|
+
const stats: string[] = [];
|
|
125
|
+
if (step.toolCount !== undefined) stats.push(`${step.toolCount} tools`);
|
|
126
|
+
if (step.tokens) stats.push(`${formatTokens(step.tokens.total)} tok`);
|
|
127
|
+
if (step.durationMs !== undefined) stats.push(formatDuration(step.durationMs));
|
|
128
|
+
return stats.join(" · ");
|
|
129
|
+
}
|
|
130
|
+
|
|
79
131
|
function resolveRunPath(asyncDir: string, filePath: string): string {
|
|
80
132
|
return path.isAbsolute(filePath) ? filePath : path.join(asyncDir, filePath);
|
|
81
133
|
}
|
|
@@ -178,7 +230,8 @@ function readRecentEvents(eventsPath: string, limit: number): { events: string[]
|
|
|
178
230
|
export class SubagentsStatusComponent implements Component {
|
|
179
231
|
private readonly width = 84;
|
|
180
232
|
private readonly viewportHeight = 12;
|
|
181
|
-
private readonly listRunsForOverlay: (asyncDirRoot: string,
|
|
233
|
+
private readonly listRunsForOverlay: (asyncDirRoot: string, options?: AsyncRunOverlayOptions) => AsyncRunOverlayData;
|
|
234
|
+
private readonly sessionId: string;
|
|
182
235
|
private readonly refreshTimer: NodeJS.Timeout;
|
|
183
236
|
private screen: "list" | "detail" = "list";
|
|
184
237
|
private cursor = 0;
|
|
@@ -197,12 +250,13 @@ export class SubagentsStatusComponent implements Component {
|
|
|
197
250
|
tui: TUI,
|
|
198
251
|
theme: Theme,
|
|
199
252
|
done: () => void,
|
|
200
|
-
deps: StatusOverlayDeps
|
|
253
|
+
deps: StatusOverlayDeps,
|
|
201
254
|
) {
|
|
202
255
|
this.tui = tui;
|
|
203
256
|
this.theme = theme;
|
|
204
257
|
this.done = done;
|
|
205
258
|
this.listRunsForOverlay = deps.listRunsForOverlay ?? listAsyncRunsForOverlay;
|
|
259
|
+
this.sessionId = deps.sessionId;
|
|
206
260
|
const refreshMs = deps.refreshMs ?? AUTO_REFRESH_MS;
|
|
207
261
|
this.reload();
|
|
208
262
|
this.refreshTimer = setInterval(() => {
|
|
@@ -215,7 +269,7 @@ export class SubagentsStatusComponent implements Component {
|
|
|
215
269
|
private reload(): void {
|
|
216
270
|
const previousSelectedId = selectedRun(this.rows, this.cursor)?.id;
|
|
217
271
|
try {
|
|
218
|
-
const overlayData = this.listRunsForOverlay(ASYNC_DIR, 5);
|
|
272
|
+
const overlayData = this.listRunsForOverlay(ASYNC_DIR, { recentLimit: 5, sessionId: this.sessionId });
|
|
219
273
|
this.active = overlayData.active;
|
|
220
274
|
this.recent = overlayData.recent;
|
|
221
275
|
this.rows = buildRows(this.active, this.recent);
|
|
@@ -282,6 +336,13 @@ export class SubagentsStatusComponent implements Component {
|
|
|
282
336
|
return lines;
|
|
283
337
|
}
|
|
284
338
|
|
|
339
|
+
private formatStepActivity(step: AsyncRunStep): string {
|
|
340
|
+
if (!step.lastActivityAt) return "";
|
|
341
|
+
if (step.activityState === "needs_attention") return `no activity for ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))}`;
|
|
342
|
+
if (step.activityState === "active_long_running") return `active but long-running; last activity ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`;
|
|
343
|
+
return `active ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`;
|
|
344
|
+
}
|
|
345
|
+
|
|
285
346
|
private renderStepRows(run: AsyncRunSummary, width: number, innerW: number, options: { wrap?: boolean } = {}): string[] {
|
|
286
347
|
const lines: string[] = [];
|
|
287
348
|
for (const step of run.steps) {
|
|
@@ -291,14 +352,8 @@ export class SubagentsStatusComponent implements Component {
|
|
|
291
352
|
: "";
|
|
292
353
|
const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
|
|
293
354
|
const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
|
|
294
|
-
const activity = step
|
|
295
|
-
|
|
296
|
-
? ` | no activity for ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))}`
|
|
297
|
-
: step.activityState === "active_long_running"
|
|
298
|
-
? ` | active but long-running; last activity ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`
|
|
299
|
-
: ` | active ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`
|
|
300
|
-
: "";
|
|
301
|
-
const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity}${model}${attempts}${duration}${tokens}`;
|
|
355
|
+
const activity = this.formatStepActivity(step);
|
|
356
|
+
const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity ? ` | ${activity}` : ""}${model}${attempts}${duration}${tokens}`;
|
|
302
357
|
if (options.wrap) {
|
|
303
358
|
lines.push(...detailRows(line, width, innerW, this.theme));
|
|
304
359
|
} else {
|
|
@@ -318,6 +373,57 @@ export class SubagentsStatusComponent implements Component {
|
|
|
318
373
|
return lines;
|
|
319
374
|
}
|
|
320
375
|
|
|
376
|
+
private renderStructuredStepRow(prefix: string, step: AsyncRunStep, width: number, innerW: number, errorIndent: string): string[] {
|
|
377
|
+
const suffix = [this.formatStepActivity(step), step.model, compactStepStats(step)].filter(Boolean).join(" · ");
|
|
378
|
+
const lines = detailRows(`${prefix}${step.agent} · ${stepStatusColor(this.theme, step.status)}${suffix ? ` · ${suffix}` : ""}`, width, innerW, this.theme);
|
|
379
|
+
if (step.error) lines.push(...detailRows(`${errorIndent}${step.error}`, width, innerW, this.theme));
|
|
380
|
+
return lines;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private renderAgentRows(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
384
|
+
if (run.steps.length === 0) return [row(this.theme.fg("dim", " No agent details available yet."), width, this.theme)];
|
|
385
|
+
const lines: string[] = [];
|
|
386
|
+
const total = run.steps.length;
|
|
387
|
+
for (const [index, step] of run.steps.entries()) {
|
|
388
|
+
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Agent ${index + 1}/${total}: `, step, width, innerW, " "));
|
|
389
|
+
}
|
|
390
|
+
return lines;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private renderChainProgressRows(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
394
|
+
if (run.steps.length === 0) return [row(this.theme.fg("dim", " No step details available yet."), width, this.theme)];
|
|
395
|
+
const lines: string[] = [];
|
|
396
|
+
const spans = buildChainStepSpans(run);
|
|
397
|
+
const total = run.chainStepCount ?? spans.length;
|
|
398
|
+
for (const span of spans) {
|
|
399
|
+
const steps = run.steps.slice(span.start, span.start + span.count);
|
|
400
|
+
const status = aggregateStepStatus(steps);
|
|
401
|
+
if (span.isParallel) {
|
|
402
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
403
|
+
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
404
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
405
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
406
|
+
const outcomeCounts = [`${done}/${span.count} done`];
|
|
407
|
+
if (failed > 0) outcomeCounts.push(`${failed} failed`);
|
|
408
|
+
if (paused > 0) outcomeCounts.push(`${paused} paused`);
|
|
409
|
+
if (running > 0) outcomeCounts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
410
|
+
const label = `${stepGlyph(this.theme, status)} Step ${span.stepIndex + 1}/${total}: parallel group · ${outcomeCounts.join(" · ")}`;
|
|
411
|
+
lines.push(...detailRows(` ${label}`, width, innerW, this.theme));
|
|
412
|
+
for (const [localIndex, step] of steps.entries()) {
|
|
413
|
+
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Agent ${localIndex + 1}/${span.count}: `, step, width, innerW, " "));
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const step = steps[0];
|
|
418
|
+
if (!step) {
|
|
419
|
+
lines.push(row(this.theme.fg("dim", ` ◦ Step ${span.stepIndex + 1}/${total}: pending`), width, this.theme));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Step ${span.stepIndex + 1}/${total}: `, step, width, innerW, " "));
|
|
423
|
+
}
|
|
424
|
+
return lines;
|
|
425
|
+
}
|
|
426
|
+
|
|
321
427
|
private renderDetail(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
322
428
|
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
323
429
|
const duration = run.endedAt !== undefined
|
|
@@ -335,8 +441,16 @@ export class SubagentsStatusComponent implements Component {
|
|
|
335
441
|
body.push(...detailRows(`${run.id} | ${statusColor(this.theme, run.state)} | ${run.mode} | ${stepLabel} | ${duration}`, width, innerW, this.theme));
|
|
336
442
|
if (activity) body.push(...detailRows(activity, width, innerW, this.theme));
|
|
337
443
|
body.push(row("", width, this.theme));
|
|
338
|
-
|
|
339
|
-
|
|
444
|
+
if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
|
|
445
|
+
body.push(row(this.theme.fg("accent", run.state === "running" ? "Chain progress" : "Chain results"), width, this.theme));
|
|
446
|
+
body.push(...this.renderChainProgressRows(run, width, innerW));
|
|
447
|
+
} else if (run.mode === "parallel") {
|
|
448
|
+
body.push(row(this.theme.fg("accent", "Agents"), width, this.theme));
|
|
449
|
+
body.push(...this.renderAgentRows(run, width, innerW));
|
|
450
|
+
} else {
|
|
451
|
+
body.push(row(this.theme.fg("accent", "Steps"), width, this.theme));
|
|
452
|
+
body.push(...this.renderStepRows(run, width, innerW, { wrap: true }));
|
|
453
|
+
}
|
|
340
454
|
|
|
341
455
|
const eventsPath = path.join(run.asyncDir, "events.jsonl");
|
|
342
456
|
const eventResult = readRecentEvents(eventsPath, DETAIL_EVENT_LIMIT);
|