pi-subagents 0.24.4 → 0.27.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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +145 -27
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/prompts/review-loop.md +1 -1
  7. package/skills/pi-subagents/SKILL.md +71 -20
  8. package/src/agents/agent-management.ts +57 -15
  9. package/src/agents/agent-serializer.ts +3 -2
  10. package/src/agents/agents.ts +47 -16
  11. package/src/agents/chain-serializer.ts +120 -0
  12. package/src/extension/fanout-child.ts +171 -0
  13. package/src/extension/index.ts +7 -2
  14. package/src/extension/schemas.ts +138 -5
  15. package/src/intercom/result-intercom.ts +108 -0
  16. package/src/runs/background/async-execution.ts +185 -10
  17. package/src/runs/background/async-job-tracker.ts +41 -6
  18. package/src/runs/background/async-resume.ts +28 -15
  19. package/src/runs/background/async-status.ts +71 -31
  20. package/src/runs/background/result-watcher.ts +111 -54
  21. package/src/runs/background/run-id-resolver.ts +83 -0
  22. package/src/runs/background/run-status.ts +89 -4
  23. package/src/runs/background/stale-run-reconciler.ts +46 -1
  24. package/src/runs/background/subagent-runner.ts +648 -42
  25. package/src/runs/foreground/chain-execution.ts +331 -118
  26. package/src/runs/foreground/execution.ts +226 -10
  27. package/src/runs/foreground/subagent-executor.ts +377 -14
  28. package/src/runs/shared/acceptance-contract.ts +291 -0
  29. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  30. package/src/runs/shared/acceptance-finalization.ts +161 -0
  31. package/src/runs/shared/acceptance-reports.ts +127 -0
  32. package/src/runs/shared/acceptance.ts +22 -0
  33. package/src/runs/shared/chain-outputs.ts +101 -0
  34. package/src/runs/shared/completion-guard.ts +26 -3
  35. package/src/runs/shared/dynamic-fanout.ts +293 -0
  36. package/src/runs/shared/nested-events.ts +819 -0
  37. package/src/runs/shared/nested-path.ts +52 -0
  38. package/src/runs/shared/nested-render.ts +115 -0
  39. package/src/runs/shared/parallel-utils.ts +31 -1
  40. package/src/runs/shared/pi-args.ts +73 -5
  41. package/src/runs/shared/structured-output.ts +77 -0
  42. package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
  43. package/src/runs/shared/workflow-graph.ts +206 -0
  44. package/src/shared/formatters.ts +2 -2
  45. package/src/shared/settings.ts +53 -4
  46. package/src/shared/types.ts +345 -0
  47. package/src/slash/slash-commands.ts +41 -3
  48. package/src/tui/render.ts +268 -43
package/src/tui/render.ts CHANGED
@@ -12,12 +12,16 @@ import {
12
12
  type AsyncJobStep,
13
13
  type AsyncParallelGroupStatus,
14
14
  type Details,
15
+ type NestedRunSummary,
16
+ type NestedStepSummary,
17
+ type WorkflowNodeStatus,
15
18
  MAX_WIDGET_JOBS,
16
19
  WIDGET_KEY,
17
20
  } from "../shared/types.ts";
18
21
  import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
19
22
  import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
20
23
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
24
+ import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
21
25
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
22
26
 
23
27
  type Theme = ExtensionContext["ui"]["theme"];
@@ -218,10 +222,21 @@ function firstOutputLine(text: string): string {
218
222
  return text.split("\n").find((line) => line.trim())?.trim() ?? "";
219
223
  }
220
224
 
225
+ function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
226
+ const acceptance = result.acceptance;
227
+ if (!acceptance?.status || acceptance.status === "not-required") return undefined;
228
+ const finalization = acceptance.finalization
229
+ ? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
230
+ : "";
231
+ return `acceptance: ${acceptance.status}${finalization}`;
232
+ }
233
+
221
234
  function resultStatusLine(result: Details["results"][number], output: string): string {
222
235
  if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
223
236
  if (result.interrupted) return "Paused";
224
237
  if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
238
+ const acceptance = formatAcceptanceStatus(result);
239
+ if (acceptance) return `Done · ${acceptance}`;
225
240
  if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
226
241
  return "Done";
227
242
  }
@@ -257,6 +272,7 @@ export function widgetRenderKey(job: AsyncJobState): string {
257
272
  chainStepCount: job.chainStepCount,
258
273
  parallelGroups: job.parallelGroups,
259
274
  steps: job.steps,
275
+ nestedChildren: job.nestedChildren,
260
276
  stepsTotal: job.stepsTotal,
261
277
  runningSteps: job.runningSteps,
262
278
  completedSteps: job.completedSteps,
@@ -399,18 +415,21 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
399
415
  return lines;
400
416
  }
401
417
 
402
- function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
418
+ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
403
419
  if (!job.steps?.length) return [];
404
420
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
405
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
421
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
406
422
  const total = job.stepsTotal ?? job.steps.length;
407
- return job.steps.map((step, index) => {
408
- const marker = index === job.steps!.length - 1 ? "└" : "├";
423
+ const lines: string[] = [];
424
+ for (const [index, step] of job.steps.entries()) {
425
+ const marker = index === job.steps.length - 1 ? "└" : "├";
409
426
  const activity = widgetStepActivity(step, job.updatedAt);
410
427
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
411
428
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
412
- return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
413
- });
429
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
430
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
431
+ }
432
+ return lines;
414
433
  }
415
434
 
416
435
  function parseParallelGroupAgentCount(label: string | undefined): number | undefined {
@@ -420,26 +439,44 @@ function parseParallelGroupAgentCount(label: string | undefined): number | undef
420
439
  return inner.split("+").map((part) => part.trim()).filter(Boolean).length;
421
440
  }
422
441
 
423
- function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">): boolean {
424
- if (details.mode !== "chain") return false;
425
- if (details.currentStepIndex === undefined) return false;
426
- const currentLabel = details.chainAgents?.[details.currentStepIndex];
427
- return parseParallelGroupAgentCount(currentLabel) !== undefined;
428
- }
429
-
430
442
  interface ChainStepSpan {
431
443
  stepIndex: number;
432
444
  start: number;
433
445
  count: number;
434
446
  isParallel: boolean;
447
+ status?: WorkflowNodeStatus;
448
+ label?: string;
449
+ error?: string;
435
450
  }
436
451
 
437
- function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[] {
438
- if (!chainAgents?.length) return [];
452
+ function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGraph">): ChainStepSpan[] {
453
+ if (details.workflowGraph?.nodes?.length) {
454
+ const spans: ChainStepSpan[] = [];
455
+ let flatCursor = 0;
456
+ for (const node of details.workflowGraph.nodes) {
457
+ if (node.stepIndex === undefined) continue;
458
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
459
+ const childFlatIndexes = (node.children ?? [])
460
+ .map((child) => child.flatIndex)
461
+ .filter((value): value is number => typeof value === "number");
462
+ const start = childFlatIndexes.length ? Math.min(...childFlatIndexes) : flatCursor;
463
+ const count = node.children?.length ?? 0;
464
+ spans.push({ stepIndex: node.stepIndex, start, count, isParallel: true, status: node.status, label: node.label, error: node.error });
465
+ flatCursor = Math.max(flatCursor, start + count);
466
+ continue;
467
+ }
468
+ const start = node.flatIndex ?? flatCursor;
469
+ spans.push({ stepIndex: node.stepIndex, start, count: 1, isParallel: false, status: node.status, label: node.label, error: node.error });
470
+ flatCursor = Math.max(flatCursor, start + 1);
471
+ }
472
+ if (spans.length) return spans.sort((left, right) => left.stepIndex - right.stepIndex);
473
+ }
474
+
475
+ if (!details.chainAgents?.length) return [];
439
476
  const spans: ChainStepSpan[] = [];
440
477
  let start = 0;
441
- for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
442
- const label = chainAgents[stepIndex]!;
478
+ for (let stepIndex = 0; stepIndex < details.chainAgents.length; stepIndex++) {
479
+ const label = details.chainAgents[stepIndex]!;
443
480
  const parsedCount = parseParallelGroupAgentCount(label);
444
481
  const count = parsedCount ?? 1;
445
482
  spans.push({ stepIndex, start, count, isParallel: parsedCount !== undefined });
@@ -448,6 +485,12 @@ function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[]
448
485
  return spans;
449
486
  }
450
487
 
488
+ function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
489
+ if (details.mode !== "chain") return false;
490
+ if (details.currentStepIndex === undefined) return false;
491
+ return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
492
+ }
493
+
451
494
  function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
452
495
  const spans: ChainStepSpan[] = [];
453
496
  let flatIndex = 0;
@@ -472,6 +515,55 @@ function isDoneResult(result: Details["results"][number]): boolean {
472
515
  return result.exitCode === 0;
473
516
  }
474
517
 
518
+ function workflowGraphHasStatus(details: Pick<Details, "workflowGraph">, statuses: WorkflowNodeStatus[]): boolean {
519
+ return details.workflowGraph?.nodes.some((node) => statuses.includes(node.status)) ?? false;
520
+ }
521
+
522
+ interface ChainRenderResultEntry {
523
+ kind: "result";
524
+ resultIndex: number;
525
+ rowNumber: number;
526
+ agentName: string;
527
+ }
528
+
529
+ interface ChainRenderPlaceholderEntry {
530
+ kind: "placeholder";
531
+ rowNumber: number;
532
+ stepLabel: string;
533
+ agentName: string;
534
+ status: WorkflowNodeStatus;
535
+ error?: string;
536
+ }
537
+
538
+ type ChainRenderEntry = ChainRenderResultEntry | ChainRenderPlaceholderEntry;
539
+
540
+ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
541
+ if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
542
+ const entries: ChainRenderEntry[] = [];
543
+ for (const span of buildChainStepSpans(details)) {
544
+ if (span.isParallel && span.count === 0) {
545
+ entries.push({
546
+ kind: "placeholder",
547
+ rowNumber: span.stepIndex + 1,
548
+ stepLabel: `Step ${span.stepIndex + 1}`,
549
+ agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
550
+ status: span.status ?? "pending",
551
+ error: span.error,
552
+ });
553
+ continue;
554
+ }
555
+ for (let index = span.start; index < span.start + span.count; index++) {
556
+ entries.push({
557
+ kind: "result",
558
+ resultIndex: index,
559
+ rowNumber: index + 1,
560
+ agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
561
+ });
562
+ }
563
+ }
564
+ return entries;
565
+ }
566
+
475
567
  interface MultiProgressLabel {
476
568
  headerLabel: string;
477
569
  itemTitle: "Step" | "Agent";
@@ -483,8 +575,8 @@ interface MultiProgressLabel {
483
575
  showActiveGroupOnly: boolean;
484
576
  }
485
577
 
486
- function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents">, hasRunning: boolean): MultiProgressLabel {
487
- const stepSpans = buildChainStepSpans(details.chainAgents);
578
+ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
579
+ const stepSpans = buildChainStepSpans(details);
488
580
  const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
489
581
  const activeParallelGroup = isChainParallelGroupActive(details);
490
582
  const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
@@ -548,11 +640,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
548
640
  if (details.mode === "chain" && details.chainAgents?.length) {
549
641
  const totalCount = details.totalSteps ?? details.chainAgents.length;
550
642
  const doneLogical = stepSpans.filter((span) => {
643
+ if (span.status && span.status !== "completed") return false;
644
+ if (span.count === 0) return span.status === "completed";
551
645
  for (let index = span.start; index < span.start + span.count; index++) {
552
646
  const progressEntry = details.progress?.find((progress) => progress.index === index);
553
647
  const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
554
- if (progressEntry?.status === "running" || progressEntry?.status === "pending") return false;
555
- if (resultEntry && !isDoneResult(resultEntry)) return false;
648
+ if (progressEntry?.status === "running" || progressEntry?.status === "pending" || progressEntry?.status === "failed") return false;
649
+ if (!resultEntry || !isDoneResult(resultEntry)) return false;
556
650
  }
557
651
  return true;
558
652
  }).length;
@@ -568,9 +662,9 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
568
662
  return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
569
663
  }
570
664
 
571
- function resultRowLabel(details: Pick<Details, "mode" | "chainAgents">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
665
+ function resultRowLabel(details: Pick<Details, "mode" | "chainAgents" | "workflowGraph">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
572
666
  if (details.mode === "chain" && label.hasParallelInChain) {
573
- const span = buildChainStepSpans(details.chainAgents).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
667
+ const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
574
668
  if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
575
669
  if (span) return `Step ${span.stepIndex + 1}`;
576
670
  }
@@ -646,6 +740,84 @@ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["s
646
740
  return path.join(job.asyncDir, `output-${step.index}.log`);
647
741
  }
648
742
 
743
+ function nestedRunName(run: NestedRunSummary): string {
744
+ if (run.agent) return run.agent;
745
+ if (run.agents?.length) return formatWidgetAgents(run.agents);
746
+ return run.id;
747
+ }
748
+
749
+ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number): string {
750
+ if (state === "running") return theme.fg("accent", runningGlyph(seed));
751
+ if (state === "complete" || state === "completed") return theme.fg("success", "✓");
752
+ if (state === "failed") return theme.fg("error", "✗");
753
+ if (state === "paused") return theme.fg("warning", "■");
754
+ return theme.fg("muted", "◦");
755
+ }
756
+
757
+ function nestedRunSeed(run: NestedRunSummary): number | undefined {
758
+ return runningSeed(run.lastUpdate, run.lastActivityAt, run.currentStep, run.toolCount, run.turnCount, run.totalTokens?.total, run.currentToolStartedAt);
759
+ }
760
+
761
+ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount">, state: NestedRunSummary["state"] | NestedStepSummary["status"], snapshotNow?: number): string {
762
+ const facts: string[] = [];
763
+ if (input.currentTool && input.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${input.currentTool} ${formatDuration(Math.max(0, snapshotNow - input.currentToolStartedAt))}`);
764
+ else if (input.currentTool) facts.push(input.currentTool);
765
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
766
+ if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
767
+ if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
768
+ const activity = buildLiveStatusLine(input, snapshotNow);
769
+ if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
770
+ if (activity) return activity;
771
+ if (facts.length) return facts.join(" · ");
772
+ if (state === "running") return "thinking…";
773
+ if (state === "queued" || state === "pending") return "queued…";
774
+ if (state === "paused") return "Paused";
775
+ if (state === "failed") return "Failed";
776
+ return "Done";
777
+ }
778
+
779
+ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
780
+ if (!children?.length || lineBudget <= 0) return [];
781
+ if (!expanded) {
782
+ const aggregate = formatNestedAggregate(children);
783
+ return aggregate ? [theme.fg("dim", `↳ ${aggregate}`)] : [];
784
+ }
785
+ const lines: string[] = [];
786
+ const maxDepth = 2;
787
+ const append = (items: NestedRunSummary[] | undefined, depth: number, prefix: string): void => {
788
+ if (!items?.length || lines.length >= lineBudget) return;
789
+ if (depth > maxDepth) {
790
+ const aggregate = formatNestedAggregate(items);
791
+ if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix}↳ ${aggregate}`));
792
+ return;
793
+ }
794
+ for (let index = 0; index < items.length; index++) {
795
+ const child = items[index]!;
796
+ if (lines.length >= lineBudget) {
797
+ const aggregate = formatNestedAggregate(items.slice(index));
798
+ if (aggregate) lines[lines.length - 1] = theme.fg("dim", `${prefix}↳ ${aggregate}`);
799
+ return;
800
+ }
801
+ const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
802
+ const error = child.error ? ` · ${child.error}` : "";
803
+ lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
804
+ if (depth === maxDepth) {
805
+ const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
806
+ if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
807
+ continue;
808
+ }
809
+ for (const step of child.steps ?? []) {
810
+ if (lines.length >= lineBudget) return;
811
+ lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
812
+ append(step.children, depth + 1, `${prefix} `);
813
+ }
814
+ append(child.children, depth + 1, `${prefix} `);
815
+ }
816
+ };
817
+ append(children, 0, "");
818
+ return lines.map((line) => truncLine(line, width));
819
+ }
820
+
649
821
  function foregroundStyleWidgetStepLines(
650
822
  job: AsyncJobState,
651
823
  theme: Theme,
@@ -662,6 +834,9 @@ function foregroundStyleWidgetStepLines(
662
834
  const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
663
835
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
664
836
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
837
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
838
+ lines.push(` ${nestedLine}`);
839
+ }
665
840
  if (step.status === "running") {
666
841
  if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
667
842
  const output = widgetOutputPath(job, step);
@@ -683,7 +858,10 @@ function foregroundStyleWidgetStepLines(
683
858
  }
684
859
 
685
860
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
686
- if (!job.steps?.length) return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
861
+ if (!job.steps?.length) return [
862
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
863
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
864
+ ];
687
865
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
688
866
  const total = job.stepsTotal ?? job.steps.length;
689
867
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
@@ -691,6 +869,11 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
691
869
  for (const [index, step] of job.steps.entries()) {
692
870
  lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
693
871
  }
872
+ const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
873
+ const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
874
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
875
+ lines.push(` ${nestedLine}`);
876
+ }
694
877
  return lines;
695
878
  }
696
879
 
@@ -720,6 +903,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
720
903
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
721
904
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
722
905
  lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
906
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
723
907
  }
724
908
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
725
909
  return lines.map((line) => truncLine(line, width));
@@ -777,7 +961,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
777
961
  items.push([
778
962
  `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
779
963
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
780
- ...widgetParallelAgentDetails(job, theme),
964
+ ...widgetParallelAgentDetails(job, theme, expanded, width),
781
965
  ]);
782
966
  slots--;
783
967
  }
@@ -794,7 +978,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
794
978
  items.push([
795
979
  `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
796
980
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
797
- ...widgetParallelAgentDetails(job, theme),
981
+ ...widgetParallelAgentDetails(job, theme, expanded, width),
798
982
  ]);
799
983
  slots--;
800
984
  }
@@ -841,7 +1025,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
841
1025
  const isRunning = r.progress?.status === "running";
842
1026
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
843
1027
  const stats = statJoin(theme, [
844
- r.usage?.turns ? `⟳${r.usage.turns}` : "",
1028
+ r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
845
1029
  formatProgressStats(theme, progress),
846
1030
  ]);
847
1031
  const c = new Container();
@@ -873,9 +1057,12 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
873
1057
 
874
1058
  function renderMultiCompact(d: Details, theme: Theme): Component {
875
1059
  const hasRunning = d.progress?.some((p) => p.status === "running")
876
- || d.results.some((r) => r.progress?.status === "running");
877
- const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
878
- const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
1060
+ || d.results.some((r) => r.progress?.status === "running")
1061
+ || workflowGraphHasStatus(d, ["running"]);
1062
+ const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
1063
+ || workflowGraphHasStatus(d, ["failed"]);
1064
+ const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
1065
+ || workflowGraphHasStatus(d, ["paused", "detached"]);
879
1066
  let totalSummary = d.progressSummary;
880
1067
  if (!totalSummary) {
881
1068
  let sawProgress = false;
@@ -908,13 +1095,29 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
908
1095
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
909
1096
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
910
1097
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
911
- for (let i = displayStart; i < displayEnd; i++) {
1098
+ const chainEntries = buildChainRenderEntries(d, multiLabel);
1099
+ const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
1100
+ const i = displayStart + offset;
912
1101
  const r = d.results[i];
913
1102
  const fallbackLabel = itemTitle.toLowerCase();
914
1103
  const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
915
- const agentName = useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`);
1104
+ return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
1105
+ });
1106
+ for (const entry of renderEntries) {
1107
+ if (entry.kind === "placeholder") {
1108
+ const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
1109
+ const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1110
+ c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1111
+ if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
1112
+ continue;
1113
+ }
1114
+ const i = entry.resultIndex;
1115
+ const r = d.results[i];
1116
+ const rowNumber = entry.rowNumber;
1117
+ const agentName = entry.agentName;
916
1118
  if (!r) {
917
- c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${itemTitle} ${rowNumber}: ${agentName} · pending`), width), 0, 0));
1119
+ const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1120
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
918
1121
  continue;
919
1122
  }
920
1123
  const output = getSingleResultOutput(r);
@@ -1062,20 +1265,27 @@ export function renderSubagentResult(
1062
1265
  if (!expanded) return renderMultiCompact(d, theme);
1063
1266
 
1064
1267
  const hasRunning = d.progress?.some((p) => p.status === "running")
1065
- || d.results.some((r) => r.progress?.status === "running");
1268
+ || d.results.some((r) => r.progress?.status === "running")
1269
+ || workflowGraphHasStatus(d, ["running"]);
1066
1270
  const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
1067
1271
  const hasEmptyWithoutTarget = d.results.some((r) =>
1068
1272
  r.exitCode === 0
1069
1273
  && r.progress?.status !== "running"
1070
1274
  && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1071
1275
  );
1276
+ const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
1277
+ const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
1072
1278
  const icon = hasRunning
1073
1279
  ? theme.fg("warning", "running")
1074
1280
  : hasEmptyWithoutTarget
1075
1281
  ? theme.fg("warning", "warning")
1076
- : ok === d.results.length
1077
- ? theme.fg("success", "ok")
1078
- : theme.fg("error", "failed");
1282
+ : hasWorkflowFailure
1283
+ ? theme.fg("error", "failed")
1284
+ : hasWorkflowPause
1285
+ ? theme.fg("warning", "paused")
1286
+ : ok === d.results.length
1287
+ ? theme.fg("success", "ok")
1288
+ : theme.fg("error", "failed");
1079
1289
 
1080
1290
  const totalSummary =
1081
1291
  d.progressSummary ||
@@ -1146,18 +1356,33 @@ export function renderSubagentResult(
1146
1356
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
1147
1357
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
1148
1358
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
1359
+ const chainEntries = buildChainRenderEntries(d, multiLabel);
1360
+ const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
1361
+ const i = displayStart + offset;
1362
+ const r = d.results[i];
1363
+ const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1364
+ return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
1365
+ });
1149
1366
 
1150
1367
  c.addChild(new Spacer(1));
1151
1368
 
1152
- for (let i = displayStart; i < displayEnd; i++) {
1369
+ for (const entry of renderEntries) {
1370
+ if (entry.kind === "placeholder") {
1371
+ const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1372
+ c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}: ${theme.bold(entry.agentName)}`), 0, 0));
1373
+ c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
1374
+ if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
1375
+ c.addChild(new Spacer(1));
1376
+ continue;
1377
+ }
1378
+ const i = entry.resultIndex;
1153
1379
  const r = d.results[i];
1154
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1155
- const agentName = useResultsDirectly
1156
- ? (r?.agent || `step-${rowNumber}`)
1157
- : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
1380
+ const rowNumber = entry.rowNumber;
1381
+ const agentName = entry.agentName;
1158
1382
 
1159
1383
  if (!r) {
1160
- c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${rowNumber}: ${agentName}`)), 0, 0));
1384
+ const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1385
+ c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
1161
1386
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
1162
1387
  c.addChild(new Spacer(1));
1163
1388
  continue;