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.
Files changed (40) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +12 -11
  3. package/agents/context-builder.md +4 -5
  4. package/agents/delegate.md +2 -1
  5. package/agents/oracle.md +5 -8
  6. package/agents/planner.md +2 -3
  7. package/agents/researcher.md +2 -3
  8. package/agents/reviewer.md +4 -21
  9. package/agents/scout.md +2 -3
  10. package/agents/worker.md +7 -7
  11. package/package.json +3 -2
  12. package/prompts/parallel-context-build.md +1 -1
  13. package/prompts/parallel-handoff-plan.md +2 -2
  14. package/skills/pi-subagents/SKILL.md +30 -27
  15. package/src/extension/index.ts +4 -1
  16. package/src/intercom/intercom-bridge.ts +10 -8
  17. package/src/intercom/result-intercom.ts +4 -3
  18. package/src/manager-ui/agent-manager-edit.ts +43 -19
  19. package/src/manager-ui/agent-manager.ts +5 -2
  20. package/src/runs/background/async-execution.ts +16 -10
  21. package/src/runs/background/async-job-tracker.ts +12 -19
  22. package/src/runs/background/async-resume.ts +1 -0
  23. package/src/runs/background/async-status.ts +35 -49
  24. package/src/runs/background/parallel-groups.ts +45 -0
  25. package/src/runs/background/result-watcher.ts +2 -2
  26. package/src/runs/background/run-status.ts +26 -7
  27. package/src/runs/background/stale-run-reconciler.ts +15 -2
  28. package/src/runs/background/subagent-runner.ts +12 -3
  29. package/src/runs/foreground/chain-clarify.ts +35 -30
  30. package/src/runs/foreground/chain-execution.ts +9 -6
  31. package/src/runs/foreground/execution.ts +4 -0
  32. package/src/runs/foreground/subagent-executor.ts +18 -30
  33. package/src/runs/shared/model-fallback.ts +2 -5
  34. package/src/runs/shared/pi-args.ts +20 -0
  35. package/src/shared/model-info.ts +68 -0
  36. package/src/shared/session-identity.ts +10 -0
  37. package/src/shared/types.ts +14 -5
  38. package/src/slash/slash-commands.ts +8 -7
  39. package/src/tui/render.ts +67 -2
  40. 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
+ }
@@ -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: "single" | "parallel" | "chain";
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: "single" | "parallel" | "chain" | "management";
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
- mode: "single" | "chain";
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?: "single" | "chain";
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: "single" | "parallel" | "chain";
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((m) => ({
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.status === "running") parts.push(formatAgentRunningLabel(running));
465
- if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
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
- listRunsForOverlay?: (asyncDirRoot: string, recentLimit?: number) => AsyncRunOverlayData;
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, recentLimit?: number) => AsyncRunOverlayData;
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.lastActivityAt
295
- ? step.activityState === "needs_attention"
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
- body.push(row(this.theme.fg("accent", "Steps"), width, this.theme));
339
- body.push(...this.renderStepRows(run, width, innerW, { wrap: true }));
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);