pi-subagents 0.59.0 → 0.60.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 (43) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/tool-reference.md +4 -1
  3. package/docs/workflows.md +2 -2
  4. package/package.json +1 -1
  5. package/skills/council-mode/SKILL.md +48 -243
  6. package/skills/council-mode/references/pass-contracts.md +150 -0
  7. package/skills/pi-subagents/SKILL.md +87 -37
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  9. package/skills/pi-subagents/references/execution-controls.md +47 -6
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  11. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  13. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  14. package/src/agents/agent-management.ts +154 -25
  15. package/src/api/shared-types.ts +2 -0
  16. package/src/extension/public-execution.ts +1 -0
  17. package/src/extension/schemas.ts +1 -0
  18. package/src/extension/tool-description.ts +8 -4
  19. package/src/runs/background/async-execution.ts +2 -2
  20. package/src/runs/background/async-job-tracker.ts +3 -0
  21. package/src/runs/background/async-status.ts +45 -2
  22. package/src/runs/background/control-channel.ts +3 -2
  23. package/src/runs/background/run-status.ts +13 -2
  24. package/src/runs/background/subagent-runner.ts +5 -1
  25. package/src/runs/background/subagent-wait.ts +10 -2
  26. package/src/runs/background/wait-completions.ts +3 -0
  27. package/src/runs/foreground/execution.ts +11 -2
  28. package/src/runs/foreground/subagent-executor.ts +98 -1
  29. package/src/runs/shared/async-status-projection.ts +138 -4
  30. package/src/runs/shared/background-process-options.ts +9 -0
  31. package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  32. package/src/runs/shared/mutation-evidence.ts +52 -3
  33. package/src/runs/shared/pi-args.ts +47 -1
  34. package/src/runs/shared/single-output.ts +45 -18
  35. package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
  36. package/src/runs/shared/workflow-graph.ts +15 -0
  37. package/src/shared/types.ts +34 -1
  38. package/src/tui/fleet-status.ts +11 -3
  39. package/src/tui/render-helpers.ts +31 -0
  40. package/src/tui/render.ts +597 -112
  41. package/src/watchdog/change-signature.ts +40 -1
  42. package/src/workflows/host-command.ts +6 -1
  43. package/src/workflows/scripted-workflow.ts +53 -2
package/src/tui/render.ts CHANGED
@@ -31,11 +31,13 @@ import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
31
31
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
32
32
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
33
33
  import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode.ts";
34
+ import { shouldSuppressSingleStep, stripRepeatedAgentPrefix, withDuplicateLabelDiscriminators } from "./render-helpers.ts";
34
35
  import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
35
36
  import { formatWorkflowPreflight, formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary, formatWorkflowPreflightWarnings } from "../workflows/workflow-preflight.ts";
36
37
  import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status-snapshot.ts";
37
38
  import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
38
39
  import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
40
+ import { workflowGraphStageNodes } from "../runs/shared/workflow-graph.ts";
39
41
 
40
42
  type Theme = ExtensionContext["ui"]["theme"];
41
43
 
@@ -192,7 +194,10 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
192
194
 
193
195
  function runningGlyph(seed?: number): string {
194
196
  if (seed === undefined) return STATIC_RUNNING_GLYPH;
195
- return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
197
+ // Free-running wall-clock spinner: advance ~8 fps regardless of model
198
+ // activity; keep the seed so parallel rows stay phase-offset.
199
+ const clock = Math.floor(Date.now() / 125);
200
+ return RUNNING_FRAMES[Math.abs(seed + clock) % RUNNING_FRAMES.length]!;
196
201
  }
197
202
 
198
203
  function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
@@ -259,6 +264,7 @@ const noisyStatusPatterns = [
259
264
  /^(?:checking|fetching|reading|inspecting|verifying|collecting|confirming|polling)\b/i,
260
265
  /^(?:async\s+subagent\s+)?[\w.-]+\s*·\s*(?:step|agent)\s+\d+\/\d+\s*·/i,
261
266
  /^(?:Step|Agent)\s+\d+\/\d+:\s+[\w.-]+\s*·\s*(?:running|queued|pending|complete|completed)\b/i,
267
+ /^(?:async\s+subagent\s+)?[\w.-]+(?:\s+\[(?:fresh|fork|mixed)\])?\s*·\s*(?:running|queued|pending|complete|completed|done)\b/i,
262
268
  /^Press\s+\S+\s+for\s+live\s+detail$/i,
263
269
  /^output:\s+.+\/async-subagent-runs\//i,
264
270
  ];
@@ -309,9 +315,129 @@ function boundedLaneValue(value: string | undefined, maxChars = LANE_VALUE_MAX_C
309
315
  return previewDisplayText(oneLine(value), maxChars);
310
316
  }
311
317
 
318
+ function workflowNodeStepStatus(status: WorkflowNodeStatus): AsyncJobStep["status"] {
319
+ switch (status) {
320
+ case "completed":
321
+ return "completed";
322
+ case "detached":
323
+ return "paused";
324
+ default:
325
+ return status;
326
+ }
327
+ }
328
+
329
+ function workflowNodeStatusLabel(status: WorkflowNodeStatus): string {
330
+ switch (status) {
331
+ case "completed":
332
+ return "complete";
333
+ case "detached":
334
+ return "paused";
335
+ default:
336
+ return status;
337
+ }
338
+ }
339
+
340
+ function workflowStepPriority(step: AsyncJobStep, currentNodeId?: string): number {
341
+ const isCurrent = step.workflowKey === currentNodeId;
342
+ if (step.status === "running" || (isCurrent && step.status !== "complete" && step.status !== "completed")) return 0;
343
+ const gate = laneGate(step);
344
+ if (
345
+ step.status === "failed"
346
+ || step.status === "partial"
347
+ || step.status === "paused"
348
+ || step.status === "stopped"
349
+ || step.status === "rejected"
350
+ || step.toolBudgetBlocked === true
351
+ || step.turnBudgetExceeded === true
352
+ || step.activityState === "needs_attention"
353
+ || step.watchdog?.phase === "stale"
354
+ || gate !== undefined
355
+ ) return 1;
356
+ if (step.status === "pending") return 2;
357
+ return 3;
358
+ }
359
+
360
+ /** Merge materialized child rows with the planned workflow stages for rendering. */
361
+ function workflowWidgetSteps(job: AsyncJobState): AsyncJobStep[] {
362
+ const planned = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
363
+ const loaded = job.steps ?? [];
364
+ if (planned.length === 0) return loaded;
365
+
366
+ const loadedIndexesByKey = new Map<string, number[]>();
367
+ for (const [index, step] of loaded.entries()) {
368
+ if (!step.workflowKey) continue;
369
+ const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
370
+ indexes.push(index);
371
+ loadedIndexesByKey.set(step.workflowKey, indexes);
372
+ }
373
+ const consumed = new Set<number>();
374
+ const entries: Array<{ step: AsyncJobStep; order: number }> = [];
375
+ for (const [order, node] of planned.entries()) {
376
+ const loadedIndex = loadedIndexesByKey.get(node.id)?.find((index) => !consumed.has(index));
377
+ if (loadedIndex !== undefined) {
378
+ consumed.add(loadedIndex);
379
+ const loadedStep = loaded[loadedIndex]!;
380
+ entries.push({
381
+ step: {
382
+ ...loadedStep,
383
+ index: node.flatIndex ?? loadedStep.index ?? order,
384
+ agent: loadedStep.agent || node.agent || job.agents?.[0] || "workflow",
385
+ phase: loadedStep.phase ?? node.phase,
386
+ label: loadedStep.label ?? node.label,
387
+ workflowKey: loadedStep.workflowKey ?? node.id,
388
+ ...(loadedStep.outputName === undefined && node.outputName !== undefined ? { outputName: node.outputName } : {}),
389
+ ...(loadedStep.structured === undefined && node.structured !== undefined ? { structured: node.structured } : {}),
390
+ ...(loadedStep.error === undefined && node.error !== undefined ? { error: node.error } : {}),
391
+ },
392
+ order,
393
+ });
394
+ continue;
395
+ }
396
+ entries.push({
397
+ step: {
398
+ index: node.flatIndex ?? order,
399
+ agent: node.agent ?? job.agents?.[0] ?? "workflow",
400
+ status: workflowNodeStepStatus(node.status),
401
+ workflowKey: node.id,
402
+ label: node.label,
403
+ ...(node.phase ? { phase: node.phase } : {}),
404
+ ...(node.outputName ? { outputName: node.outputName } : {}),
405
+ ...(node.structured !== undefined ? { structured: node.structured } : {}),
406
+ ...(node.error ? { error: node.error } : {}),
407
+ },
408
+ order,
409
+ });
410
+ }
411
+ for (const [index, step] of loaded.entries()) {
412
+ if (!consumed.has(index)) entries.push({ step, order: planned.length + index });
413
+ }
414
+ entries.sort((left, right) => workflowStepPriority(left.step, job.workflowGraph?.currentNodeId) - workflowStepPriority(right.step, job.workflowGraph?.currentNodeId) || left.order - right.order);
415
+ return entries.map(({ step }) => step);
416
+ }
417
+
418
+ function workflowStageProgress(job: AsyncJobState): { total: number; current?: number } | undefined {
419
+ if (job.mode !== "workflow") return undefined;
420
+ const stages = workflowGraphStageNodes(job.workflowGraph);
421
+ if (stages.length === 0) return undefined;
422
+ const currentId = job.workflowGraph?.currentNodeId;
423
+ const currentIndex = currentId ? stages.findIndex((stage) => stage.id === currentId) : -1;
424
+ if (currentIndex >= 0 && stages[currentIndex]?.status !== "completed") return { total: stages.length, current: currentIndex };
425
+ const runningIndex = stages.findIndex((stage) => stage.status === "running");
426
+ if (runningIndex >= 0) return { total: stages.length, current: runningIndex };
427
+ const indexedStage = job.currentStep !== undefined && job.currentStep >= 0 ? stages[job.currentStep] : undefined;
428
+ if (indexedStage && indexedStage.status !== "completed") return { total: stages.length, current: job.currentStep };
429
+ if (stages.every((stage) => stage.status === "completed")) return { total: stages.length, current: stages.length - 1 };
430
+ return { total: stages.length };
431
+ }
432
+
312
433
  function laneStepForJob(job: AsyncJobState): AsyncJobStep | undefined {
313
- const steps = job.steps ?? [];
434
+ const steps = workflowWidgetSteps(job);
314
435
  if (steps.length === 0) return undefined;
436
+ const graphCurrent = job.workflowGraph?.currentNodeId;
437
+ if (graphCurrent) {
438
+ const current = steps.find((step) => step.workflowKey === graphCurrent);
439
+ if (current) return current;
440
+ }
315
441
  if (job.currentStep !== undefined) {
316
442
  const current = steps[job.currentStep];
317
443
  if (current && (current.index === undefined || current.index === job.currentStep)) return current;
@@ -454,7 +580,7 @@ function workflowPreflightLines(job: AsyncJobState, expanded = false): string[]
454
580
  }
455
581
 
456
582
  function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
457
- const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
583
+ const flatIndex = foregroundResultIndex(details, resultIndex);
458
584
  const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
459
585
  for (const node of nodes) {
460
586
  if (node.flatIndex === flatIndex && node.label.trim()) return node.label;
@@ -466,6 +592,52 @@ function workflowLabelForResult(details: Details, resultIndex: number): string |
466
592
  return details.workflowGraph ? visit(details.workflowGraph.nodes) : undefined;
467
593
  }
468
594
 
595
+ function foregroundProgressForResult(details: Details, resultIndex: number): AgentProgress | undefined {
596
+ const result = details.results[resultIndex];
597
+ if (result?.progress) return result.progress;
598
+ const index = result?.index ?? resultIndex;
599
+ const indexed = details.progress?.find((progress) => progress.index === index);
600
+ if (indexed) return indexed;
601
+ return result?.agent
602
+ ? details.progress?.find((progress) => progress.agent === result.agent && progress.status === "running")
603
+ : undefined;
604
+ }
605
+
606
+ function foregroundResultIndex(details: Details, resultIndex: number): number {
607
+ const result = details.results[resultIndex];
608
+ if (typeof result?.index === "number") return result.index;
609
+ return foregroundProgressForResult(details, resultIndex)?.index ?? resultIndex;
610
+ }
611
+
612
+ function foregroundResultDisplayName(
613
+ details: Details,
614
+ resultIndex: number,
615
+ result: Details["results"][number] | undefined,
616
+ fallback: string,
617
+ ): string {
618
+ const progress = foregroundProgressForResult(details, resultIndex);
619
+ const agent = normalizedParallelDisplayText(result?.agent) ?? normalizedParallelDisplayText(progress?.agent);
620
+ const workflowLabel = normalizedParallelDisplayText(workflowLabelForResult(details, resultIndex));
621
+ if (workflowLabel) return compactTaskText(undefined, workflowLabel) ?? workflowLabel;
622
+
623
+ const sessionName = normalizedParallelDisplayText(result?.sessionName) ?? normalizedParallelDisplayText(progress?.sessionName);
624
+ if (sessionName) {
625
+ const sessionTask = stripRepeatedAgentPrefix(sessionName, agent);
626
+ if (sessionTask && sessionTask !== PROMPT_REDACTED && sessionTask.toLowerCase() !== agent?.toLowerCase()) {
627
+ return compactTaskText(sessionTask) ?? sessionTask;
628
+ }
629
+ }
630
+
631
+ return compactTaskText(result?.task) ?? compactTaskText(progress?.task) ?? agent ?? fallback;
632
+ }
633
+
634
+ function foregroundSingleDisplayName(result: Details["results"][number] | undefined): string {
635
+ return normalizedParallelDisplayText(result?.agent)
636
+ ?? normalizedParallelDisplayText(result?.sessionName)
637
+ ?? compactTaskText(result?.task)
638
+ ?? "subagent";
639
+ }
640
+
469
641
  function hasLiveOutputSignal(line: string): boolean {
470
642
  const clean = oneLine(line);
471
643
  return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
@@ -779,6 +951,14 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
779
951
  step.review?.status,
780
952
  step.toolBudgetBlocked,
781
953
  step.turnBudgetExceeded,
954
+ step.timedOut,
955
+ step.stopped,
956
+ step.execution?.status,
957
+ step.execution?.error,
958
+ step.execution?.timedOut,
959
+ step.execution?.interrupted,
960
+ step.execution?.stopped,
961
+ step.execution?.detached,
782
962
  step.watchdog?.phase,
783
963
  step.error,
784
964
  expanded ? expandedStepActivityRenderKey(step) : undefined,
@@ -844,6 +1024,10 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
844
1024
  chainStepCount: job.chainStepCount,
845
1025
  parallelGroups: job.parallelGroups,
846
1026
  workflowHostSteps: projectAsyncWorkflowRows([], job.hostSteps).map(hostStepRenderKey),
1027
+ workflowGraph: job.mode === "workflow" && job.workflowGraph ? {
1028
+ currentNodeId: job.workflowGraph.currentNodeId,
1029
+ stages: workflowGraphStageNodes(job.workflowGraph).map((node) => [node.id, node.status, node.agent, node.phase, node.label, node.flatIndex, node.outputName, node.structured, node.error]),
1030
+ } : undefined,
847
1031
  preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
848
1032
  preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
849
1033
  steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
@@ -855,6 +1039,8 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
855
1039
  activeParallelGroup: job.activeParallelGroup,
856
1040
  startedAt: job.startedAt,
857
1041
  updatedAt: job.updatedAt,
1042
+ timedOut: job.timedOut,
1043
+ stopped: job.stopped,
858
1044
  totalTokens: job.totalTokens,
859
1045
  });
860
1046
  }
@@ -874,6 +1060,65 @@ function widgetJobName(job: AsyncJobState): string {
874
1060
  return job.mode ?? "subagent";
875
1061
  }
876
1062
 
1063
+ function isSingleChildAsyncJob(job: AsyncJobState): boolean {
1064
+ return job.mode === "single"
1065
+ && job.steps?.length === 1
1066
+ && shouldSuppressSingleStep(job.chainStepCount, job.stepsTotal);
1067
+ }
1068
+
1069
+ function isCompletedWidgetStepStatus(status: AsyncJobStep["status"]): boolean {
1070
+ return status === "complete" || status === "completed";
1071
+ }
1072
+
1073
+ function singleChildAgentName(job: AsyncJobState, step: AsyncJobStep): string {
1074
+ return job.agents?.length === 1 ? job.agents[0]! : step.agent || widgetJobName(job);
1075
+ }
1076
+
1077
+ function hasSingleChildDetailEvidence(job: AsyncJobState, step: AsyncJobStep): boolean {
1078
+ return Boolean(
1079
+ step.error?.trim()
1080
+ || step.execution?.error?.trim()
1081
+ || step.phase?.trim()
1082
+ || step.label?.trim()
1083
+ || step.workflowKey?.trim()
1084
+ || step.outputName?.trim()
1085
+ || step.lane
1086
+ || job.workflowKey?.trim()
1087
+ || job.lane
1088
+ || laneTraceForJob(job)?.phase?.trim()
1089
+ || laneTraceForJob(job)?.label?.trim()
1090
+ || step.timedOut
1091
+ || step.stopped
1092
+ || step.execution?.timedOut
1093
+ || step.execution?.interrupted
1094
+ || step.execution?.stopped
1095
+ || step.execution?.detached
1096
+ || job.timedOut
1097
+ || job.stopped
1098
+ || ["failed", "partial", "paused", "stopped", "detached"].includes(step.execution?.status ?? "")
1099
+ || step.acceptance?.status === "rejected"
1100
+ || step.acceptance?.reviewResult?.status === "blockers"
1101
+ || step.review?.status === "blockers",
1102
+ );
1103
+ }
1104
+
1105
+ function shouldCollapseSingleChildDetails(job: AsyncJobState, step: AsyncJobStep): boolean {
1106
+ if (!isSingleChildAsyncJob(job) || hasSingleChildDetailEvidence(job, step)) return false;
1107
+ if (job.status === "running") return step.status === "running";
1108
+ if (job.status === "complete") return isCompletedWidgetStepStatus(step.status);
1109
+ return false;
1110
+ }
1111
+
1112
+ function singleChildTask(job: AsyncJobState, step: AsyncJobStep): string | undefined {
1113
+ const task = compactTaskText(step.description, step.label);
1114
+ if (task) return task;
1115
+ const sessionName = step.sessionName?.trim();
1116
+ if (!sessionName) return undefined;
1117
+ const agentName = singleChildAgentName(job, step);
1118
+ const sessionTask = stripRepeatedAgentPrefix(sessionName, agentName);
1119
+ return sessionTask === agentName ? undefined : compactTaskText(sessionTask);
1120
+ }
1121
+
877
1122
  function widgetActivity(job: AsyncJobState): string {
878
1123
  const facts: string[] = [];
879
1124
  if (job.currentTool && job.currentToolStartedAt !== undefined && job.updatedAt !== undefined) facts.push(`${job.currentTool} ${formatDuration(Math.max(0, job.updatedAt - job.currentToolStartedAt))}`);
@@ -982,8 +1227,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
982
1227
  for (const span of buildAsyncChainStepSpans(total, job.steps.length, job.parallelGroups)) {
983
1228
  const steps = job.steps.slice(span.start, span.start + span.count);
984
1229
  if (span.isParallel) {
985
- const status = aggregateStepStatus(steps);
986
- lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), frame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
1230
+ lines.push(...parallelWidgetGroupDetails(job, theme, { steps, total: span.count, stepIndex: span.stepIndex, chainTotal: total }, expanded, width, frame, false));
987
1231
  continue;
988
1232
  }
989
1233
  const step = steps[0];
@@ -1000,6 +1244,8 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
1000
1244
  if (!job.steps?.length) return [];
1001
1245
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
1002
1246
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
1247
+ const group = activeParallelWidgetGroup(job);
1248
+ if (group) return parallelWidgetGroupDetails(job, theme, group, expanded, width, frame, Boolean(job.activeParallelGroup));
1003
1249
  const total = job.stepsTotal ?? job.steps.length;
1004
1250
  const lines: string[] = [];
1005
1251
  for (const [index, step] of job.steps.entries()) {
@@ -1090,6 +1336,127 @@ function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGrou
1090
1336
  return spans;
1091
1337
  }
1092
1338
 
1339
+ interface ParallelWidgetGroup {
1340
+ steps: AsyncJobStep[];
1341
+ total: number;
1342
+ stepIndex?: number;
1343
+ chainTotal?: number;
1344
+ }
1345
+
1346
+ interface ParallelWidgetStepRow {
1347
+ step: AsyncJobStep;
1348
+ index: number;
1349
+ rowLabel: string;
1350
+ }
1351
+
1352
+ function normalizedParallelDisplayText(value: string | undefined): string | undefined {
1353
+ if (!value?.trim()) return undefined;
1354
+ const normalized = oneLine(value);
1355
+ return normalized && normalized !== PROMPT_REDACTED ? normalized : undefined;
1356
+ }
1357
+
1358
+ function parallelWidgetStepDisplayName(step: AsyncJobStep): string {
1359
+ const agent = normalizedParallelDisplayText(step.agent);
1360
+ const explicitLabel = normalizedParallelDisplayText(step.label);
1361
+ if (explicitLabel) {
1362
+ const label = compactTaskText(step.description, explicitLabel) ?? explicitLabel;
1363
+ return agent ? `${label} (${agent})` : label;
1364
+ }
1365
+
1366
+ const sessionName = normalizedParallelDisplayText(step.sessionName);
1367
+ if (sessionName) {
1368
+ const sessionTask = stripRepeatedAgentPrefix(sessionName, agent);
1369
+ if (sessionTask && sessionTask !== PROMPT_REDACTED && sessionTask.toLowerCase() !== agent?.toLowerCase()) return sessionTask;
1370
+ }
1371
+
1372
+ return compactTaskText(step.description) ?? agent ?? "subagent";
1373
+ }
1374
+
1375
+ function parallelWidgetStepPriority(step: AsyncJobStep): number {
1376
+ if (step.status === "running" || step.activityState === "needs_attention") return 0;
1377
+ switch (step.status) {
1378
+ case "failed":
1379
+ case "rejected":
1380
+ case "partial":
1381
+ case "stopped":
1382
+ case "paused":
1383
+ return 1;
1384
+ case "complete":
1385
+ case "completed":
1386
+ return 2;
1387
+ default:
1388
+ return 3;
1389
+ }
1390
+ }
1391
+
1392
+ function parallelWidgetStepRows(steps: AsyncJobStep[], total: number, prioritizeActive: boolean): ParallelWidgetStepRow[] {
1393
+ const indexed = steps.map((step, index) => ({ step, index, displayName: parallelWidgetStepDisplayName(step) }));
1394
+ if (prioritizeActive) indexed.sort((left, right) => parallelWidgetStepPriority(left.step) - parallelWidgetStepPriority(right.step) || left.index - right.index);
1395
+ return withDuplicateLabelDiscriminators(indexed, total).map(({ displayName, ...row }) => row);
1396
+ }
1397
+
1398
+ function activeParallelWidgetGroup(job: AsyncJobState): ParallelWidgetGroup | undefined {
1399
+ const steps = job.steps ?? [];
1400
+ if (!steps.length) return undefined;
1401
+ if (job.mode === "parallel") return { steps, total: job.stepsTotal ?? steps.length };
1402
+ if (job.mode !== "chain" || !job.activeParallelGroup) return undefined;
1403
+
1404
+ const chainTotal = job.chainStepCount ?? job.stepsTotal ?? steps.length;
1405
+ const spans = buildAsyncChainStepSpans(chainTotal, steps.length, job.parallelGroups);
1406
+ const currentStep = job.currentStep;
1407
+ const activeSpan = currentStep === undefined
1408
+ ? spans.find((span) => span.isParallel)
1409
+ : spans.find((span) => span.isParallel
1410
+ && currentStep >= span.start
1411
+ && currentStep < span.start + span.count);
1412
+ return {
1413
+ steps,
1414
+ total: activeSpan?.count ?? job.stepsTotal ?? steps.length,
1415
+ stepIndex: activeSpan?.stepIndex,
1416
+ chainTotal,
1417
+ };
1418
+ }
1419
+
1420
+ function parallelWidgetGroupHeader(
1421
+ group: ParallelWidgetGroup,
1422
+ theme: Theme,
1423
+ frame?: number,
1424
+ ): string {
1425
+ const status = aggregateStepStatus(group.steps);
1426
+ const label = group.stepIndex !== undefined && group.chainTotal !== undefined
1427
+ ? `Step ${group.stepIndex + 1}/${group.chainTotal}: parallel group`
1428
+ : "parallel group";
1429
+ return ` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(group.steps), frame)} ${themeBold(theme, label)} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(group.steps, group.total))}`;
1430
+ }
1431
+
1432
+ function parallelWidgetGroupDetails(
1433
+ job: AsyncJobState,
1434
+ theme: Theme,
1435
+ group: ParallelWidgetGroup,
1436
+ expanded: boolean,
1437
+ width: number,
1438
+ frame: number | undefined,
1439
+ prioritizeActive: boolean,
1440
+ ): string[] {
1441
+ const lines = [parallelWidgetGroupHeader(group, theme, frame)];
1442
+ const rows = parallelWidgetStepRows(group.steps, group.total, prioritizeActive);
1443
+ for (const [rowIndex, row] of rows.entries()) {
1444
+ const marker = rowIndex === rows.length - 1 && rows.length >= group.total ? "└─" : "├─";
1445
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, row.step, "Agent", row.index + 1, group.total, expanded, width, frame, {
1446
+ rowLabel: row.rowLabel,
1447
+ rowIndent: " ",
1448
+ detailIndent: " ",
1449
+ rowMarker: marker,
1450
+ includeCurrentPath: true,
1451
+ }));
1452
+ }
1453
+ for (let index = rows.length; index < group.total; index++) {
1454
+ const marker = index === group.total - 1 ? "└─" : "├─";
1455
+ lines.push(` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", "pending")}`);
1456
+ }
1457
+ return lines;
1458
+ }
1459
+
1093
1460
  function isDoneResult(result: Details["results"][number]): boolean {
1094
1461
  const status = result.progress?.status;
1095
1462
  if (status === "completed") return true;
@@ -1108,32 +1475,72 @@ interface ChainRenderResultEntry {
1108
1475
  rowNumber: number;
1109
1476
  rowLabel?: string;
1110
1477
  agentName: string;
1478
+ displayIndex: number;
1479
+ isParallel?: boolean;
1111
1480
  }
1112
1481
 
1113
- interface ChainRenderPlaceholderEntry {
1114
- kind: "placeholder";
1115
- rowNumber: number;
1482
+ interface ChainRenderGroupEntry {
1483
+ kind: "group";
1116
1484
  stepLabel: string;
1117
- agentName: string;
1485
+ groupLabel?: string;
1118
1486
  status: WorkflowNodeStatus;
1119
1487
  error?: string;
1120
1488
  }
1121
1489
 
1122
- type ChainRenderEntry = ChainRenderResultEntry | ChainRenderPlaceholderEntry;
1490
+ type ChainRenderEntry = ChainRenderResultEntry | ChainRenderGroupEntry;
1491
+
1492
+ function chainSpanStatus(details: Details, span: ChainStepSpan): WorkflowNodeStatus {
1493
+ if (span.status) return span.status;
1494
+ const results = details.results.slice(span.start, span.start + span.count);
1495
+ if (results.some((result) => isResultRunning(result))) return "running";
1496
+ if (results.some((result) => !hasTerminalResultFlag(result) && (result.progress?.status === "failed" || result.exitCode !== 0) && !isResultRunning(result))) return "failed";
1497
+ if (results.some((result) => result.stopped)) return "stopped";
1498
+ if (results.some((result) => result.interrupted)) return "paused";
1499
+ if (results.some((result) => result.detached || result.progress?.status === "detached")) return "detached";
1500
+ if (results.length < span.count) return "pending";
1501
+ if (results.length > 0 && results.every(isDoneResult)) return "completed";
1502
+ return "pending";
1503
+ }
1504
+
1505
+ function withDuplicateForegroundLabels(entries: ChainRenderResultEntry[], total: number): ChainRenderResultEntry[] {
1506
+ return withDuplicateLabelDiscriminators(
1507
+ entries.map((entry) => ({ ...entry, index: entry.displayIndex, displayName: entry.agentName })),
1508
+ total,
1509
+ ).map(({ displayName, rowLabel, ...entry }) => ({
1510
+ ...entry,
1511
+ rowLabel: rowLabel === displayName ? undefined : rowLabel.slice(0, -(displayName.length + 2)),
1512
+ }));
1513
+ }
1123
1514
 
1124
1515
  function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
1125
1516
  if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
1126
1517
  const entries: ChainRenderEntry[] = [];
1127
1518
  for (const span of buildChainStepSpans(details)) {
1128
- if (span.isParallel && span.count === 0) {
1519
+ if (span.isParallel) {
1129
1520
  entries.push({
1130
- kind: "placeholder",
1131
- rowNumber: span.stepIndex + 1,
1132
- stepLabel: `Step ${span.stepIndex + 1}`,
1133
- agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
1134
- status: span.status ?? "pending",
1521
+ kind: "group",
1522
+ stepLabel: `Step ${span.stepIndex + 1}/${label.logicalStepCount}: parallel group`,
1523
+ groupLabel: span.label?.trim() || undefined,
1524
+ status: chainSpanStatus(details, span),
1135
1525
  error: span.error,
1136
1526
  });
1527
+ const groupEntries: ChainRenderResultEntry[] = [];
1528
+ for (let index = span.start; index < span.start + span.count; index++) {
1529
+ const result = details.results[index];
1530
+ const localIndex = foregroundResultIndex(details, index);
1531
+ const displayIndex = localIndex >= span.start && localIndex < span.start + span.count
1532
+ ? localIndex - span.start
1533
+ : index - span.start;
1534
+ groupEntries.push({
1535
+ kind: "result",
1536
+ resultIndex: index,
1537
+ rowNumber: index - span.start + 1,
1538
+ agentName: foregroundResultDisplayName(details, index, result, `agent-${displayIndex + 1}`),
1539
+ displayIndex,
1540
+ isParallel: true,
1541
+ });
1542
+ }
1543
+ entries.push(...withDuplicateForegroundLabels(groupEntries, span.count));
1137
1544
  continue;
1138
1545
  }
1139
1546
  for (let index = span.start; index < span.start + span.count; index++) {
@@ -1141,9 +1548,10 @@ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): C
1141
1548
  entries.push({
1142
1549
  kind: "result",
1143
1550
  resultIndex: index,
1144
- rowNumber: index + 1,
1145
- rowLabel: span.isParallel ? `Agent ${index - span.start + 1}/${span.count}` : `Step ${span.stepIndex + 1}`,
1146
- agentName: result?.sessionName?.trim() || result?.agent || details.chainAgents?.[span.stepIndex] || `step-${span.stepIndex + 1}`,
1551
+ rowNumber: span.stepIndex + 1,
1552
+ rowLabel: resultRowLabel(label, span.stepIndex + 1),
1553
+ agentName: foregroundResultDisplayName(details, index, result, details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`),
1554
+ displayIndex: foregroundResultIndex(details, index),
1147
1555
  });
1148
1556
  }
1149
1557
  }
@@ -1159,6 +1567,7 @@ interface MultiProgressLabel {
1159
1567
  groupStartIndex: number;
1160
1568
  groupEndIndex: number;
1161
1569
  showActiveGroupOnly: boolean;
1570
+ logicalStepCount: number;
1162
1571
  }
1163
1572
 
1164
1573
  function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
@@ -1194,7 +1603,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
1194
1603
  const headerLabel = hasRunning
1195
1604
  ? `${formatAgentRunningLabel(running)} · ${done}/${totalCount} done`
1196
1605
  : `${done}/${totalCount} done`;
1197
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: totalCount, showActiveGroupOnly: false };
1606
+ return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: totalCount, showActiveGroupOnly: false, logicalStepCount: totalCount };
1198
1607
  }
1199
1608
 
1200
1609
  if (activeParallelGroup) {
@@ -1222,7 +1631,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
1222
1631
  const headerLabel = hasRunning
1223
1632
  ? `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${formatAgentRunningLabel(running)} · ${done}/${groupSize} done`
1224
1633
  : `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${done}/${groupSize} done`;
1225
- return { headerLabel, itemTitle, totalCount: groupSize, hasParallelInChain, activeParallelGroup, groupStartIndex: groupStart, groupEndIndex: groupEnd, showActiveGroupOnly: true };
1634
+ return { headerLabel, itemTitle, totalCount: groupSize, hasParallelInChain, activeParallelGroup, groupStartIndex: groupStart, groupEndIndex: groupEnd, showActiveGroupOnly: true, logicalStepCount: totalSteps };
1226
1635
  }
1227
1636
 
1228
1637
  if (details.mode === "chain" && details.chainAgents?.length) {
@@ -1240,30 +1649,66 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
1240
1649
  }).length;
1241
1650
  const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, doneLogical + (hasRunning ? 1 : 0));
1242
1651
  const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${doneLogical}/${totalCount}`;
1243
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
1652
+ return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false, logicalStepCount: totalCount };
1244
1653
  }
1245
1654
 
1246
1655
  const totalCount = details.totalSteps ?? details.results.length;
1247
1656
  const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, details.results.filter(isDoneResult).length + (hasRunning ? 1 : 0));
1248
1657
  const done = details.results.filter(isDoneResult).length;
1249
1658
  const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${done}/${totalCount}`;
1250
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
1251
- }
1252
-
1253
- function resultRowLabel(label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
1254
- if (label.itemTitle === "Agent") {
1255
- const localStepNumber = label.activeParallelGroup
1256
- ? resultIndex - label.groupStartIndex + 1
1257
- : stepNumber;
1258
- return `Agent ${localStepNumber}/${label.totalCount}`;
1259
- }
1260
- return `Step ${stepNumber}`;
1659
+ return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false, logicalStepCount: totalCount };
1660
+ }
1661
+
1662
+ function resultRowLabel(label: MultiProgressLabel, stepNumber: number): string | undefined {
1663
+ if (label.itemTitle === "Agent") return undefined;
1664
+ if (shouldSuppressSingleStep(label.logicalStepCount)) return undefined;
1665
+ return `Step ${stepNumber}/${label.logicalStepCount}`;
1666
+ }
1667
+
1668
+ function buildForegroundResultEntries(
1669
+ details: Details,
1670
+ label: MultiProgressLabel,
1671
+ displayStart: number,
1672
+ displayEnd: number,
1673
+ useResultsDirectly: boolean,
1674
+ ): ChainRenderResultEntry[] {
1675
+ const fallbackLabel = label.itemTitle.toLowerCase();
1676
+ const entries = Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderResultEntry => {
1677
+ const index = displayStart + offset;
1678
+ const result = details.results[index];
1679
+ const stableIndex = foregroundResultIndex(details, index);
1680
+ const rowNumber = label.showActiveGroupOnly ? index - label.groupStartIndex + 1 : stableIndex + 1;
1681
+ const fallbackAgent = useResultsDirectly
1682
+ ? (result?.agent || `${fallbackLabel}-${rowNumber}`)
1683
+ : (details.chainAgents![index] || result?.agent || `${fallbackLabel}-${rowNumber}`);
1684
+ const displayIndex = label.activeParallelGroup && stableIndex >= label.groupStartIndex && stableIndex < label.groupEndIndex
1685
+ ? stableIndex - label.groupStartIndex
1686
+ : label.activeParallelGroup ? index - label.groupStartIndex : stableIndex;
1687
+ return {
1688
+ kind: "result",
1689
+ resultIndex: index,
1690
+ rowNumber,
1691
+ rowLabel: resultRowLabel(label, rowNumber),
1692
+ agentName: foregroundResultDisplayName(details, index, result, fallbackAgent),
1693
+ displayIndex,
1694
+ };
1695
+ });
1696
+ return label.itemTitle === "Agent" ? withDuplicateForegroundLabels(entries, label.totalCount) : entries;
1261
1697
  }
1262
1698
 
1263
1699
  function widgetStats(job: AsyncJobState, theme: Theme): string {
1264
1700
  const parts: string[] = [];
1265
- const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
1266
- if (job.activeParallelGroup) {
1701
+ const stageProgress = workflowStageProgress(job);
1702
+ const stepsTotal = stageProgress?.total ?? job.stepsTotal ?? (job.agents?.length ?? 1);
1703
+ const isSingleChild = isSingleChildAsyncJob(job);
1704
+ if (stageProgress) {
1705
+ const stages = workflowGraphStageNodes(job.workflowGraph);
1706
+ const currentStage = stageProgress.current !== undefined ? stages[stageProgress.current] : undefined;
1707
+ const focus = currentStage
1708
+ ? [compactTaskText(undefined, currentStage.label) ?? boundedLaneValue(currentStage.id), currentStage.agent ? boundedLaneValue(currentStage.agent) : "", workflowNodeStatusLabel(currentStage.status)].filter(Boolean)
1709
+ : [];
1710
+ parts.push(["staged lane", stageProgress.current !== undefined ? `stage ${stageProgress.current + 1}/${stageProgress.total}` : `${stageProgress.total} stages`, ...focus].join(" · "));
1711
+ } else if (job.activeParallelGroup) {
1267
1712
  const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
1268
1713
  const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
1269
1714
  if (job.mode === "parallel") {
@@ -1280,10 +1725,10 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
1280
1725
  parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`);
1281
1726
  }
1282
1727
  } else if (job.currentStep !== undefined) {
1283
- if (job.mode === "chain" && job.parallelGroups?.length) {
1728
+ if (job.mode === "chain") {
1284
1729
  const total = job.chainStepCount ?? stepsTotal;
1285
- parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`);
1286
- } else {
1730
+ parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups ?? []) + 1}/${total}`);
1731
+ } else if (!isSingleChild) {
1287
1732
  parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
1288
1733
  }
1289
1734
  } else if (stepsTotal > 1) {
@@ -1487,40 +1932,57 @@ function foregroundStyleWidgetStepLines(
1487
1932
  job: AsyncJobState,
1488
1933
  theme: Theme,
1489
1934
  step: NonNullable<AsyncJobState["steps"]>[number],
1490
- itemTitle: "Agent" | "Step",
1935
+ itemTitle: "Agent" | "Stage" | "Step",
1491
1936
  index: number,
1492
1937
  total: number,
1493
1938
  expanded: boolean,
1494
1939
  width: number,
1495
1940
  frame?: number,
1941
+ options?: { rowLabel?: string; rowIndent?: string; detailIndent?: string; rowMarker?: string; includeCurrentPath?: boolean },
1496
1942
  ): string[] {
1943
+ const rowIndent = options?.rowIndent ?? " ";
1944
+ const detailIndent = options?.detailIndent ?? " ";
1497
1945
  const status = widgetStepStatus(step.status, theme);
1498
1946
  const stats = widgetStepStats(theme, step);
1499
1947
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1500
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, childDisplayName(step))}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1948
+ const collapseDetails = shouldCollapseSingleChildDetails(job, step);
1949
+ const displayName = collapseDetails ? singleChildAgentName(job, step) : childDisplayName(step);
1950
+ const stageName = itemTitle === "Stage"
1951
+ ? compactTaskText(undefined, step.label) ?? boundedLaneValue(step.workflowKey) ?? displayName
1952
+ : undefined;
1953
+ const stageIdentity = stageName && stageName !== displayName ? `${stageName} (${displayName})` : stageName ?? displayName;
1954
+ const rowLabel = collapseDetails ? displayName : (options?.rowLabel ?? `${itemTitle} ${index}/${total}: ${itemTitle === "Stage" ? stageIdentity : displayName}`);
1955
+ const rowMarker = options?.rowMarker ? `${options.rowMarker} ` : "";
1956
+ const lines = [`${rowIndent}${rowMarker}${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${themeBold(theme, rowLabel)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1501
1957
  const lane = projectAsyncLane(job, step);
1502
- if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
1503
- const task = compactTaskText(step.description, step.label);
1504
- if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
1958
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, detailIndent));
1959
+ const task = collapseDetails ? singleChildTask(job, step) : compactTaskText(step.description, step.label);
1960
+ if (task) lines.push(`${detailIndent}${theme.fg("dim", `task: ${task}`)}`);
1505
1961
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
1506
- if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
1962
+ const currentPath = options?.includeCurrentPath && step.currentPath ? shortenPath(step.currentPath) : undefined;
1963
+ const activityWithPath = currentPath && activity && !activity.includes(currentPath)
1964
+ ? `${activity} · ${currentPath}`
1965
+ : activity ?? currentPath;
1966
+ if (activityWithPath) lines.push(`${detailIndent}${theme.fg("dim", `⎿ ${activityWithPath}`)}`);
1507
1967
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
1508
- lines.push(` ${nestedLine}`);
1968
+ lines.push(`${detailIndent}${nestedLine}`);
1509
1969
  }
1970
+ const error = step.error?.trim() || step.execution?.error?.trim();
1971
+ if (error) lines.push(`${detailIndent}${theme.fg("error", `error: ${oneLine(error)}`)}`);
1510
1972
  if (step.status === "running") {
1511
- if (!expanded) lines.push(` ${theme.fg("accent", liveDetailHintText())}`);
1973
+ if (!expanded) lines.push(`${detailIndent}${theme.fg("accent", liveDetailHintText())}`);
1512
1974
  const output = widgetOutputPath(job, step);
1513
- if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
1975
+ if (output) lines.push(`${detailIndent}${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
1514
1976
  if (expanded) {
1515
1977
  const liveStatus = buildLiveStatusLine(step, job.updatedAt);
1516
- if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
1978
+ if (liveStatus && liveStatus !== activity) lines.push(`${detailIndent}${theme.fg("accent", liveStatus)}`);
1517
1979
  for (const tool of step.recentTools?.slice(-3) ?? []) {
1518
1980
  const maxArgsLen = Math.max(40, width - 30);
1519
1981
  const argsPreview = renderToolArgsPreview(tool.args, maxArgsLen, expanded);
1520
- lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
1982
+ lines.push(`${detailIndent} ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
1521
1983
  }
1522
1984
  for (const line of compactRecentOutputLines(step.recentOutput)) {
1523
- lines.push(` ${theme.fg("dim", line)}`);
1985
+ lines.push(`${detailIndent} ${theme.fg("dim", line)}`);
1524
1986
  }
1525
1987
  }
1526
1988
  }
@@ -1553,7 +2015,8 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
1553
2015
  }
1554
2016
 
1555
2017
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
1556
- if (!job.steps?.length) {
2018
+ const steps = workflowWidgetSteps(job);
2019
+ if (!steps.length) {
1557
2020
  const lane = projectAsyncLane(job);
1558
2021
  return [
1559
2022
  ...workflowPreflightLines(job, expanded),
@@ -1564,14 +2027,31 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
1564
2027
  ];
1565
2028
  }
1566
2029
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
1567
- const total = job.stepsTotal ?? job.steps.length;
1568
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
1569
2030
  const lines: string[] = workflowPreflightLines(job, expanded);
1570
- for (const [index, step] of job.steps.entries()) {
1571
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, frame));
2031
+ const group = activeParallelWidgetGroup(job);
2032
+ if (group) {
2033
+ lines.push(...parallelWidgetGroupDetails(job, theme, group, expanded, width, frame, Boolean(job.activeParallelGroup)));
2034
+ } else {
2035
+ const stageProgress = workflowStageProgress(job);
2036
+ const total = job.mode === "chain"
2037
+ ? job.chainStepCount ?? job.stepsTotal ?? steps.length
2038
+ : stageProgress?.total ?? job.stepsTotal ?? steps.length;
2039
+ const stageTotal = stageProgress?.total ?? total;
2040
+ const plannedKeys = stageProgress ? new Set(workflowGraphStageNodes(job.workflowGraph).map((node) => node.id)) : undefined;
2041
+ const extraStepCount = plannedKeys
2042
+ ? steps.filter((step) => step.workflowKey === undefined || !plannedKeys.has(step.workflowKey)).length
2043
+ : total;
2044
+ let extraStepIndex = 0;
2045
+ for (const [index, step] of steps.entries()) {
2046
+ const isPlannedStage = plannedKeys !== undefined && step.workflowKey !== undefined && plannedKeys.has(step.workflowKey);
2047
+ const itemTitle = isPlannedStage ? "Stage" : "Step";
2048
+ const displayIndex = isPlannedStage ? (step.index ?? index) + 1 : plannedKeys ? ++extraStepIndex : index + 1;
2049
+ const displayTotal = isPlannedStage ? stageTotal : extraStepCount;
2050
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, displayIndex, displayTotal, expanded, width, frame));
2051
+ }
1572
2052
  }
1573
2053
  lines.push(...hostStepWidgetLines(job, theme, " "));
1574
- const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
2054
+ const attached = new Set(steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
1575
2055
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
1576
2056
  for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
1577
2057
  lines.push(` ${nestedLine}`);
@@ -1581,12 +2061,16 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
1581
2061
 
1582
2062
  function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
1583
2063
  const stats = widgetStats(job, theme);
1584
- const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
2064
+ const count = job.mode === "chain" ? job.chainStepCount : workflowStageProgress(job)?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
1585
2065
  const mode = widgetJobName(job);
1586
- const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
2066
+ const title = isSingleChildAsyncJob(job)
2067
+ ? "async subagent"
2068
+ : `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
2069
+ const collapseDetails = job.steps?.length === 1 && shouldCollapseSingleChildDetails(job, job.steps[0]!);
2070
+ const summary = `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`;
1587
2071
  return [
1588
2072
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
1589
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2073
+ ...(collapseDetails ? [] : [summary]),
1590
2074
  ...foregroundStyleWidgetDetails(job, theme, expanded, width, frame),
1591
2075
  ].map((line) => truncLine(line, width));
1592
2076
  }
@@ -1595,10 +2079,13 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1595
2079
  const fullLines = buildSingleWidgetLines(job, theme, width, false, frame);
1596
2080
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
1597
2081
 
1598
- const total = job.stepsTotal ?? job.steps.length;
1599
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
2082
+ const group = activeParallelWidgetGroup(job);
2083
+ if (!group) return fullLines;
2084
+ const rows = parallelWidgetStepRows(group.steps, group.total, Boolean(job.activeParallelGroup));
1600
2085
  const lines = fullLines.slice(0, 2);
1601
- for (const [index, step] of job.steps.entries()) {
2086
+ lines.push(parallelWidgetGroupHeader(group, theme, frame));
2087
+ for (const [rowIndex, row] of rows.entries()) {
2088
+ const step = row.step;
1602
2089
  const status = widgetStepStatus(step.status, theme);
1603
2090
  const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
1604
2091
  const stepStats = widgetStepStats(theme, step);
@@ -1606,10 +2093,11 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1606
2093
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1607
2094
  const task = compactTaskText(step.description, step.label);
1608
2095
  const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
1609
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, childDisplayName(step))}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
2096
+ const marker = rowIndex === rows.length - 1 && rowIndex >= group.total - 1 ? "└─" : "├─";
2097
+ lines.push(` ${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, row.index), frame)} ${themeBold(theme, row.rowLabel)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1610
2098
  const lane = projectAsyncLane(job, step);
1611
- if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
1612
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
2099
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
2100
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
1613
2101
  }
1614
2102
  lines.push(...hostStepWidgetLines(job, theme, " "));
1615
2103
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
@@ -1852,6 +2340,10 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
1852
2340
  widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
1853
2341
  return fitWidgetLineBudget(lines, theme, width, false);
1854
2342
  }
2343
+ if (availableRows > 2 && jobs.length === 1 && workflowStageProgress(jobs[0]!)) {
2344
+ widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
2345
+ return fitWidgetLineBudget(lines, theme, width, false);
2346
+ }
1855
2347
 
1856
2348
  if (availableRows <= 2) {
1857
2349
  widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
@@ -1898,9 +2390,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1898
2390
  let hiddenFinished = 0;
1899
2391
  let queuedSummaryShown = false;
1900
2392
  let slots = MAX_WIDGET_JOBS;
1901
-
1902
- for (const job of running) {
1903
- if (slots <= 0) { hiddenRunning++; continue; }
2393
+ const appendJob = (job: AsyncJobState): void => {
1904
2394
  const stats = widgetStats(job, theme);
1905
2395
  items.push([
1906
2396
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
@@ -1908,6 +2398,11 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1908
2398
  ...widgetLaneDetailLines(job, theme),
1909
2399
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1910
2400
  ]);
2401
+ };
2402
+
2403
+ for (const job of running) {
2404
+ if (slots <= 0) { hiddenRunning++; continue; }
2405
+ appendJob(job);
1911
2406
  slots--;
1912
2407
  }
1913
2408
 
@@ -1919,13 +2414,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1919
2414
 
1920
2415
  for (const job of finished) {
1921
2416
  if (slots <= 0) { hiddenFinished++; continue; }
1922
- const stats = widgetStats(job, theme);
1923
- items.push([
1924
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1925
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1926
- ...widgetLaneDetailLines(job, theme),
1927
- ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1928
- ]);
2417
+ appendJob(job);
1929
2418
  slots--;
1930
2419
  }
1931
2420
 
@@ -1991,7 +2480,7 @@ function renderSingleCompact(
1991
2480
  const detailIndent = mainWindowIndent(layout, 1);
1992
2481
  const continuationIndent = mainWindowIndent(layout, 2) + (layout.horizontalSpacing > 0 ? " " : "");
1993
2482
  const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1994
- c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(childDisplayName(r)))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
2483
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(foregroundSingleDisplayName(r)))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1995
2484
 
1996
2485
  if (isRunning && r.progress) {
1997
2486
  const task = compactTaskText(r.task);
@@ -2144,19 +2633,13 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
2144
2633
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
2145
2634
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
2146
2635
  const chainEntries = buildChainRenderEntries(d, multiLabel);
2147
- const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
2148
- const i = displayStart + offset;
2149
- const r = d.results[i];
2150
- const fallbackLabel = itemTitle.toLowerCase();
2151
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
2152
- const fallbackAgent = useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`);
2153
- return { kind: "result", resultIndex: i, rowNumber, agentName: r?.sessionName?.trim() || fallbackAgent };
2154
- });
2636
+ const renderEntries = chainEntries ?? buildForegroundResultEntries(d, multiLabel, displayStart, displayEnd, useResultsDirectly);
2155
2637
  for (const entry of renderEntries) {
2156
- if (entry.kind === "placeholder") {
2638
+ if (entry.kind === "group") {
2157
2639
  const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
2158
2640
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
2159
- c.addChild(new Text(truncLine(`${rowIndent}${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
2641
+ const groupLabel = entry.groupLabel ? ` (${compactTaskText(undefined, entry.groupLabel) ?? entry.groupLabel})` : "";
2642
+ c.addChild(new Text(truncLine(`${rowIndent}${glyph} ${entry.stepLabel}${groupLabel} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
2160
2643
  if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", `${detailIndent}⎿ Error: ${entry.error}`), width), 0, 0));
2161
2644
  continue;
2162
2645
  }
@@ -2165,23 +2648,24 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
2165
2648
  const rowNumber = entry.rowNumber;
2166
2649
  const agentName = entry.agentName;
2167
2650
  if (!r) {
2168
- const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
2169
- c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
2651
+ const pendingLabel = entry.rowLabel ?? (entry.isParallel ? "" : `${itemTitle} ${rowNumber}`);
2652
+ const labelPrefix = pendingLabel ? `${pendingLabel}: ` : "";
2653
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ ${labelPrefix}${agentName} · pending`), width), 0, 0));
2170
2654
  continue;
2171
2655
  }
2172
2656
  const output = getSingleResultOutput(r);
2173
- const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
2657
+ const progressFromArray = foregroundProgressForResult(d, i);
2174
2658
  const rProg = r.progress || progressFromArray || r.progressSummary;
2175
2659
  const rRunning = rProg && "status" in rProg && isResultRunning(r, rProg.status);
2176
2660
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
2177
- const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
2178
2661
  const stepStats = formatProgressStats(theme, rProg);
2179
2662
  const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
2180
2663
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
2181
- const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
2664
+ const stepLabel = entry.rowLabel;
2182
2665
  const rowProgressModel = rProg && "status" in rProg ? rProg : undefined;
2183
2666
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
2184
- const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
2667
+ const labelPrefix = stepLabel ? `${stepLabel}: ` : "";
2668
+ const line = `${glyph} ${labelPrefix}${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
2185
2669
  c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
2186
2670
  if (rRunning || rPending) {
2187
2671
  const task = compactTaskText(r.task, workflowLabelForResult(d, i));
@@ -2242,7 +2726,7 @@ export function renderSubagentSummary(
2242
2726
  ? theme.fg("error", "✗")
2243
2727
  : theme.fg("warning", "■");
2244
2728
  const label = details?.mode === "single" && results.length === 1
2245
- ? childDisplayName(results[0])
2729
+ ? foregroundSingleDisplayName(results[0])
2246
2730
  : details?.mode || "subagent";
2247
2731
  return new Text(
2248
2732
  truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("dim", "·")} ${theme.fg(state === "failed" ? "error" : state === "completed" ? "success" : state === "running" ? "accent" : "warning", state)}`, getTermWidth() - 4),
@@ -2315,7 +2799,7 @@ export function renderSubagentResult(
2315
2799
  const fit = (text: string) => expanded ? text : truncLine(text, w);
2316
2800
  const toolCallLines = getToolCallLines(r, expanded);
2317
2801
  const c = new Container();
2318
- c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(childDisplayName(r)))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
2802
+ c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(foregroundSingleDisplayName(r)))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
2319
2803
  c.addChild(new Spacer(1));
2320
2804
  const taskMaxLen = Math.max(20, w - 8);
2321
2805
  const taskPreview = expanded || r.task.length <= taskMaxLen
@@ -2325,6 +2809,9 @@ export function renderSubagentResult(
2325
2809
  new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
2326
2810
  );
2327
2811
  c.addChild(new Spacer(1));
2812
+ if (!isRunning && (r.exitCode !== 0 || r.interrupted || r.detached || r.stopped)) {
2813
+ c.addChild(new Text(fit(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`)), 0, 0));
2814
+ }
2328
2815
 
2329
2816
  if (isRunning && r.progress) {
2330
2817
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
@@ -2456,7 +2943,7 @@ export function renderSubagentResult(
2456
2943
  ? d.chainAgents
2457
2944
  .map((agent, i) => {
2458
2945
  const result = d.results[i];
2459
- const displayName = result?.sessionName?.trim() || agent;
2946
+ const displayName = foregroundResultDisplayName(d, i, result, agent);
2460
2947
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
2461
2948
  const stepPresentation = result
2462
2949
  ? styledResultPresentation(resultPresentation(result, getSingleResultOutput(result), isCurrent && hasRunning && !hasTerminalResultFlag(result)), theme)
@@ -2487,20 +2974,15 @@ export function renderSubagentResult(
2487
2974
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
2488
2975
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
2489
2976
  const chainEntries = buildChainRenderEntries(d, multiLabel);
2490
- const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
2491
- const i = displayStart + offset;
2492
- const r = d.results[i];
2493
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
2494
- const fallbackAgent = useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
2495
- return { kind: "result", resultIndex: i, rowNumber, agentName: r?.sessionName?.trim() || fallbackAgent };
2496
- });
2977
+ const renderEntries = chainEntries ?? buildForegroundResultEntries(d, multiLabel, displayStart, displayEnd, useResultsDirectly);
2497
2978
 
2498
2979
  c.addChild(new Spacer(1));
2499
2980
 
2500
2981
  for (const entry of renderEntries) {
2501
- if (entry.kind === "placeholder") {
2982
+ if (entry.kind === "group") {
2502
2983
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
2503
- c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}: ${theme.bold(entry.agentName)}`), 0, 0));
2984
+ const groupLabel = entry.groupLabel ? ` (${compactTaskText(undefined, entry.groupLabel) ?? entry.groupLabel})` : "";
2985
+ c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}${groupLabel}`), 0, 0));
2504
2986
  c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
2505
2987
  if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
2506
2988
  c.addChild(new Spacer(1));
@@ -2512,28 +2994,28 @@ export function renderSubagentResult(
2512
2994
  const agentName = entry.agentName;
2513
2995
 
2514
2996
  if (!r) {
2515
- const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
2516
- c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
2997
+ const pendingLabel = entry.rowLabel ?? (entry.isParallel ? "" : `${itemTitle} ${rowNumber}`);
2998
+ const labelPrefix = pendingLabel ? `${pendingLabel}: ` : "";
2999
+ c.addChild(new Text(fit(theme.fg("dim", ` ${labelPrefix}${agentName}`)), 0, 0));
2517
3000
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
2518
3001
  c.addChild(new Spacer(1));
2519
3002
  continue;
2520
3003
  }
2521
3004
 
2522
- const progressFromArray = d.progress?.find((p) => p.index === i)
2523
- || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
3005
+ const progressFromArray = foregroundProgressForResult(d, i);
2524
3006
  const rProg = r.progress || progressFromArray || r.progressSummary;
2525
3007
  const rRunning = isResultRunning(r, rProg?.status);
2526
- const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
2527
3008
 
2528
3009
  const resultOutput = getSingleResultOutput(r);
2529
3010
  const rowPresentation = styledResultPresentation(resultPresentation(r, resultOutput, rRunning, progressRunningSeed(rProg), frame), theme);
2530
3011
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
2531
3012
  const modelDisplay = modelThinkingBadge(theme, r.model ?? rProg?.model, r.thinking ?? rProg?.thinking);
2532
- const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
3013
+ const stepLabel = entry.rowLabel;
2533
3014
  const contextBadge = contextModeBadge(theme, r.context);
3015
+ const labelPrefix = stepLabel ? `${stepLabel}: ` : "";
2534
3016
  const stepHeader = rRunning
2535
- ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", childDisplayName(r)))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
2536
- : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(childDisplayName(r))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
3017
+ ? `${rowPresentation.glyph} ${labelPrefix}${theme.bold(theme.fg("warning", agentName))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
3018
+ : `${rowPresentation.glyph} ${labelPrefix}${theme.bold(agentName)}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
2537
3019
  const toolCallLines = getToolCallLines(r, expanded);
2538
3020
  c.addChild(new Text(fit(stepHeader), 0, 0));
2539
3021
 
@@ -2547,6 +3029,9 @@ export function renderSubagentResult(
2547
3029
  if (outputTarget) {
2548
3030
  c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
2549
3031
  }
3032
+ if (!rRunning && (r.exitCode !== 0 || r.interrupted || r.detached || r.stopped)) {
3033
+ c.addChild(new Text(fit(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, resultOutput)}`)), 0, 0));
3034
+ }
2550
3035
 
2551
3036
  if (r.skills?.length) {
2552
3037
  c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));