pi-subagents 0.21.4 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +12 -11
- package/agents/context-builder.md +4 -5
- package/agents/delegate.md +2 -1
- package/agents/oracle.md +5 -8
- package/agents/planner.md +2 -3
- package/agents/researcher.md +2 -3
- package/agents/reviewer.md +4 -21
- package/agents/scout.md +2 -3
- package/agents/worker.md +7 -7
- package/package.json +3 -2
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +2 -2
- package/skills/pi-subagents/SKILL.md +30 -27
- package/src/extension/index.ts +4 -1
- package/src/intercom/intercom-bridge.ts +10 -8
- package/src/intercom/result-intercom.ts +4 -3
- package/src/manager-ui/agent-manager-edit.ts +43 -19
- package/src/manager-ui/agent-manager.ts +5 -2
- 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 +12 -3
- package/src/runs/foreground/chain-clarify.ts +35 -30
- package/src/runs/foreground/chain-execution.ts +9 -6
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +18 -30
- package/src/runs/shared/model-fallback.ts +2 -5
- package/src/runs/shared/pi-args.ts +20 -0
- package/src/shared/model-info.ts +68 -0
- package/src/shared/session-identity.ts +10 -0
- package/src/shared/types.ts +14 -5
- package/src/slash/slash-commands.ts +8 -7
- package/src/tui/render.ts +67 -2
- package/src/tui/subagents-status.ts +129 -15
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
2
|
+
export type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
3
|
+
export type ThinkingLevelMap = Partial<Record<ThinkingLevel, string | null>>;
|
|
4
|
+
|
|
5
|
+
export interface ModelInfo {
|
|
6
|
+
provider: string;
|
|
7
|
+
id: string;
|
|
8
|
+
fullId: string;
|
|
9
|
+
reasoning?: boolean;
|
|
10
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RegistryModelLike {
|
|
14
|
+
provider: string;
|
|
15
|
+
id: string;
|
|
16
|
+
reasoning?: boolean;
|
|
17
|
+
thinkingLevelMap?: ThinkingLevelMap;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toModelInfo(model: RegistryModelLike): ModelInfo {
|
|
21
|
+
return {
|
|
22
|
+
provider: model.provider,
|
|
23
|
+
id: model.id,
|
|
24
|
+
fullId: `${model.provider}/${model.id}`,
|
|
25
|
+
reasoning: model.reasoning,
|
|
26
|
+
thinkingLevelMap: model.thinkingLevelMap,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function splitKnownThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
31
|
+
const colonIdx = model.lastIndexOf(":");
|
|
32
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
33
|
+
const suffix = THINKING_LEVELS.find((level) => level === model.substring(colonIdx + 1));
|
|
34
|
+
if (!suffix) return { baseModel: model, thinkingSuffix: "" };
|
|
35
|
+
return {
|
|
36
|
+
baseModel: model.substring(0, colonIdx),
|
|
37
|
+
thinkingSuffix: `:${suffix}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function findModelInfo(model: string | undefined, availableModels: ModelInfo[] | undefined, preferredProvider?: string): ModelInfo | undefined {
|
|
42
|
+
if (!model || !availableModels || availableModels.length === 0) return undefined;
|
|
43
|
+
const { baseModel } = splitKnownThinkingSuffix(model);
|
|
44
|
+
const exact = availableModels.find((entry) => entry.fullId === baseModel);
|
|
45
|
+
if (exact) return exact;
|
|
46
|
+
|
|
47
|
+
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
48
|
+
if (preferredProvider) {
|
|
49
|
+
const preferred = matches.find((entry) => entry.provider === preferredProvider);
|
|
50
|
+
if (preferred) return preferred;
|
|
51
|
+
}
|
|
52
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getSupportedThinkingLevels(model: ModelInfo | undefined): ThinkingLevel[] {
|
|
56
|
+
if (!model) return [...THINKING_LEVELS];
|
|
57
|
+
if (model.reasoning === false) return ["off"];
|
|
58
|
+
|
|
59
|
+
if (!model.thinkingLevelMap) return [...THINKING_LEVELS];
|
|
60
|
+
|
|
61
|
+
const levels = THINKING_LEVELS.filter((level) => {
|
|
62
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
63
|
+
if (mapped === null) return false;
|
|
64
|
+
if (level === "xhigh") return mapped !== undefined;
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
return levels;
|
|
68
|
+
}
|
|
@@ -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;
|
|
@@ -427,6 +435,7 @@ export interface RunSyncOptions {
|
|
|
427
435
|
onControlEvent?: (event: ControlEvent) => void;
|
|
428
436
|
controlConfig?: ResolvedControlConfig;
|
|
429
437
|
intercomSessionName?: string;
|
|
438
|
+
orchestratorIntercomTarget?: string;
|
|
430
439
|
maxOutput?: MaxOutputConfig;
|
|
431
440
|
artifactsDir?: string;
|
|
432
441
|
artifactConfig?: ArtifactConfig;
|
|
@@ -8,8 +8,10 @@ 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";
|
|
14
|
+
import { toModelInfo } from "../shared/model-info.ts";
|
|
13
15
|
import {
|
|
14
16
|
applySlashUpdate,
|
|
15
17
|
buildSlashInitialResult,
|
|
@@ -331,15 +333,11 @@ async function openAgentManager(
|
|
|
331
333
|
config: ExtensionConfig = {},
|
|
332
334
|
): Promise<void> {
|
|
333
335
|
const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
|
|
334
|
-
const models = ctx.modelRegistry.getAvailable().map(
|
|
335
|
-
provider: m.provider,
|
|
336
|
-
id: m.id,
|
|
337
|
-
fullId: `${m.provider}/${m.id}`,
|
|
338
|
-
}));
|
|
336
|
+
const models = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
339
337
|
const skills = discoverAvailableSkills(ctx.cwd);
|
|
340
338
|
|
|
341
339
|
const result = await ctx.ui.custom<ManagerResult>(
|
|
342
|
-
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut }),
|
|
340
|
+
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut, preferredModelProvider: ctx.model?.provider }),
|
|
343
341
|
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
344
342
|
);
|
|
345
343
|
if (!result) return;
|
|
@@ -571,8 +569,11 @@ export function registerSlashCommands(
|
|
|
571
569
|
pi.registerCommand("subagents-status", {
|
|
572
570
|
description: "Show active and recent async subagent runs",
|
|
573
571
|
handler: async (_args, ctx) => {
|
|
572
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
573
|
+
state.baseCwd = ctx.cwd;
|
|
574
|
+
state.currentSessionId = sessionId;
|
|
574
575
|
await ctx.ui.custom<void>(
|
|
575
|
-
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined)),
|
|
576
|
+
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
|
|
576
577
|
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
577
578
|
);
|
|
578
579
|
},
|
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);
|