pi-subagents 0.58.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
package/src/tui/render.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  type AsyncJobStep,
14
14
  type AsyncParallelGroupStatus,
15
15
  type Details,
16
+ type HostStepState,
17
+ type HostStepVerdict,
16
18
  type NestedRunSummary,
17
19
  type NestedStepSummary,
18
20
  type WorkflowNodeStatus,
@@ -29,8 +31,13 @@ import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
29
31
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
30
32
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
31
33
  import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode.ts";
34
+ import { shouldSuppressSingleStep, stripRepeatedAgentPrefix, withDuplicateLabelDiscriminators } from "./render-helpers.ts";
32
35
  import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
36
+ import { formatWorkflowPreflight, formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary, formatWorkflowPreflightWarnings } from "../workflows/workflow-preflight.ts";
33
37
  import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status-snapshot.ts";
38
+ import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
39
+ import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
40
+ import { workflowGraphStageNodes } from "../runs/shared/workflow-graph.ts";
34
41
 
35
42
  type Theme = ExtensionContext["ui"]["theme"];
36
43
 
@@ -187,7 +194,10 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
187
194
 
188
195
  function runningGlyph(seed?: number): string {
189
196
  if (seed === undefined) return STATIC_RUNNING_GLYPH;
190
- 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]!;
191
201
  }
192
202
 
193
203
  function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
@@ -254,6 +264,7 @@ const noisyStatusPatterns = [
254
264
  /^(?:checking|fetching|reading|inspecting|verifying|collecting|confirming|polling)\b/i,
255
265
  /^(?:async\s+subagent\s+)?[\w.-]+\s*·\s*(?:step|agent)\s+\d+\/\d+\s*·/i,
256
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,
257
268
  /^Press\s+\S+\s+for\s+live\s+detail$/i,
258
269
  /^output:\s+.+\/async-subagent-runs\//i,
259
270
  ];
@@ -266,6 +277,12 @@ function oneLine(text: string): string {
266
277
 
267
278
  const COMPACT_TASK_MAX_CHARS = 96;
268
279
 
280
+ /** Display label for a child run: the derived session name (agent + task
281
+ * excerpt) when the launcher provided one, else the bare agent name. */
282
+ function childDisplayName(result: { agent?: string; sessionName?: string } | undefined, fallback = "subagent"): string {
283
+ return result?.sessionName?.trim() || result?.agent || fallback;
284
+ }
285
+
269
286
  export function compactTaskText(task: string | undefined, label?: string): string | undefined {
270
287
  const taskText = task?.trim();
271
288
  const labelText = label?.trim();
@@ -278,8 +295,292 @@ export function compactTaskText(task: string | undefined, label?: string): strin
278
295
  return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
279
296
  }
280
297
 
298
+ export interface AsyncLaneProjection {
299
+ label?: string;
300
+ role: string;
301
+ phase?: string;
302
+ state: AsyncJobState["status"] | AsyncJobStep["status"];
303
+ gate?: string;
304
+ next?: string;
305
+ output?: string;
306
+ workspace?: string;
307
+ ref: string;
308
+ chips: string[];
309
+ }
310
+
311
+ const LANE_VALUE_MAX_CHARS = 48;
312
+
313
+ function boundedLaneValue(value: string | undefined, maxChars = LANE_VALUE_MAX_CHARS): string | undefined {
314
+ if (!value?.trim() || value.trim() === PROMPT_REDACTED) return undefined;
315
+ return previewDisplayText(oneLine(value), maxChars);
316
+ }
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
+
433
+ function laneStepForJob(job: AsyncJobState): AsyncJobStep | undefined {
434
+ const steps = workflowWidgetSteps(job);
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
+ }
441
+ if (job.currentStep !== undefined) {
442
+ const current = steps[job.currentStep];
443
+ if (current && (current.index === undefined || current.index === job.currentStep)) return current;
444
+ // Active parallel groups retain flat indices while exposing only the group slice.
445
+ const indexedCurrent = steps.find((step) => step.index === job.currentStep);
446
+ if (indexedCurrent) return indexedCurrent;
447
+ return steps[0];
448
+ }
449
+ return steps.find((step) => step.status === "running")
450
+ ?? steps.find((step) => step.status === "pending")
451
+ ?? steps.at(-1);
452
+ }
453
+
454
+ function laneTraceForJob(job: AsyncJobState): NonNullable<AsyncJobState["workflow"]>["trace"][number] | undefined {
455
+ return Array.isArray(job.workflow?.trace) ? job.workflow.trace.at(-1) : undefined;
456
+ }
457
+
458
+ function laneGate(step: AsyncJobStep | undefined): string | undefined {
459
+ const review = step?.review?.status ?? step?.acceptance?.reviewResult?.status;
460
+ if (review === "blockers") return "review blockers";
461
+ if (review === "review-required") return "review required";
462
+ if (review === "reviewed") return "reviewed";
463
+ const acceptance = step?.acceptance?.status;
464
+ if (acceptance === "review-required") return "acceptance review";
465
+ if (acceptance === "checked" || acceptance === "verified" || acceptance === "accepted") return "acceptance";
466
+ return undefined;
467
+ }
468
+
469
+ function laneNextAction(state: AsyncLaneProjection["state"], step: AsyncJobStep | undefined, output: string | undefined, gate: string | undefined): string | undefined {
470
+ if (step?.watchdog?.phase === "stale") return "inspect stale state";
471
+ if (step?.toolBudgetBlocked === true || step?.turnBudgetExceeded === true) return "inspect blocked state";
472
+ if (gate === "review blockers") return "resolve review blockers";
473
+ if (gate === "review required" || gate === "acceptance review") return "review output";
474
+ if (step?.activityState === "needs_attention") return "inspect attention";
475
+ if (state === "queued" || state === "pending") return "await launch";
476
+ if (state === "failed" || state === "rejected") return "inspect failure";
477
+ if (state === "partial") return "inspect partial state";
478
+ if (state === "paused") return "inspect paused state";
479
+ if (state === "stopped") return "inspect stopped state";
480
+ if ((state === "complete" || state === "completed") && gate === "reviewed") return "ready";
481
+ if ((state === "complete" || state === "completed") && output) return "inspect output";
482
+ return undefined;
483
+ }
484
+
485
+ function isTerminalLaneState(state: AsyncJobState["status"]): boolean {
486
+ return state !== "queued" && state !== "running";
487
+ }
488
+
489
+ /** Project already-loaded async status facts into one bounded, render-only lane row. */
490
+ export function projectAsyncLane(job: AsyncJobState, selectedStep = laneStepForJob(job)): AsyncLaneProjection | undefined {
491
+ const trace = laneTraceForJob(job);
492
+ const workspace = job.cwd ? boundedLaneValue(shortenPath(job.cwd)) : undefined;
493
+ const label = compactTaskText(selectedStep?.description, selectedStep?.label)
494
+ ?? boundedLaneValue(trace?.label)
495
+ ?? (workspace ? undefined : boundedLaneValue(selectedStep?.workflowKey ?? job.workflowKey));
496
+ const role = boundedLaneValue(selectedStep?.agent ?? trace?.agent ?? job.agents?.[0] ?? widgetJobName(job), 32) ?? "subagent";
497
+ const phase = boundedLaneValue(selectedStep?.phase ?? trace?.phase);
498
+ const gate = laneGate(selectedStep);
499
+ const output = boundedLaneValue(selectedStep?.outputName);
500
+ const ref = boundedLaneValue(selectedStep?.workflowKey ?? job.workflowKey ?? job.asyncId.slice(0, 8), 24) ?? job.asyncId.slice(0, 8);
501
+ const chips = [
502
+ selectedStep?.context,
503
+ selectedStep?.structured ? "structured" : undefined,
504
+ selectedStep?.activityState === "active_long_running" ? "long-running" : undefined,
505
+ selectedStep?.activityState === "needs_attention" ? "attention" : undefined,
506
+ selectedStep?.watchdog?.phase === "stale" ? "stale" : undefined,
507
+ selectedStep?.toolBudgetBlocked === true || selectedStep?.turnBudgetExceeded === true ? "blocked" : undefined,
508
+ ].filter((chip): chip is string => Boolean(chip));
509
+ const state = isTerminalLaneState(job.status) ? job.status : selectedStep?.status ?? job.status;
510
+ const next = laneNextAction(state, selectedStep, output, gate);
511
+ if (!label && !phase && !gate && !output && !selectedStep?.workflowKey && !job.workflowKey && !trace?.label && !trace?.phase) return undefined;
512
+ return { ...(label ? { label } : {}), role, ...(phase ? { phase } : {}), state, ...(gate ? { gate } : {}), ...(next ? { next } : {}), ...(output ? { output } : {}), ...(workspace ? { workspace } : {}), ref, chips };
513
+ }
514
+
515
+ function laneStateLabel(state: AsyncLaneProjection["state"], theme: Theme): string {
516
+ if (state === "running") return theme.fg("accent", "running");
517
+ if (state === "queued" || state === "pending") return theme.fg("muted", state);
518
+ if (state === "complete" || state === "completed") return theme.fg("success", "complete");
519
+ if (state === "failed" || state === "rejected") return theme.fg("error", state === "rejected" ? "rejected" : "failed");
520
+ return theme.fg("warning", state);
521
+ }
522
+
523
+ function formatLaneProjection(lane: AsyncLaneProjection, theme: Theme): string {
524
+ const label = boundedLaneValue(lane.label, 56);
525
+ const identity = [label, lane.role ? `role:${lane.role}` : undefined].filter(Boolean).join(" · ");
526
+ return `${identity ? theme.bold(identity) : theme.bold(lane.role)} · ${laneStateLabel(lane.state, theme)}`;
527
+ }
528
+
529
+ function formatLaneChip(chip: string, theme: Theme): string {
530
+ const text = `[${chip}]`;
531
+ if (chip === "blocked") return theme.fg("error", text);
532
+ if (chip === "stale") return theme.fg("warning", text);
533
+ return text;
534
+ }
535
+
536
+ function formatLaneProjectionDetails(lane: AsyncLaneProjection, theme: Theme): string | undefined {
537
+ const details = [
538
+ lane.phase ? `phase:${lane.phase}` : undefined,
539
+ lane.gate ? `gate:${lane.gate}` : undefined,
540
+ lane.next ? `next:${lane.next}` : undefined,
541
+ lane.output ? `out:${lane.output}` : undefined,
542
+ lane.workspace ? `workspace:${lane.workspace}` : lane.ref ? `ref:${lane.ref}` : undefined,
543
+ ].filter(Boolean);
544
+ const chips = lane.chips.map((chip) => formatLaneChip(chip, theme));
545
+ return [...(details.length ? [theme.fg("dim", details.join(" · "))] : []), ...chips].join(" · ") || undefined;
546
+ }
547
+
548
+ function formatLaneProjectionLines(lane: AsyncLaneProjection, theme: Theme, indent: string): string[] {
549
+ const details = formatLaneProjectionDetails(lane, theme);
550
+ return [
551
+ `${indent}${formatLaneProjection(lane, theme)}`,
552
+ ...(details ? [`${indent} ${details}`] : []),
553
+ ];
554
+ }
555
+
556
+ function laneRenderKey(job: AsyncJobState): unknown {
557
+ const lane = projectAsyncLane(job);
558
+ return lane ? [lane.label, lane.role, lane.phase, lane.state, lane.gate, lane.next, lane.output, lane.workspace, lane.ref, lane.chips] : undefined;
559
+ }
560
+
561
+ function widgetLaneDetailLines(job: AsyncJobState, theme: Theme): string[] {
562
+ if (job.steps?.length && (job.mode === "parallel" || job.mode === "chain")) return [];
563
+ const lane = projectAsyncLane(job);
564
+ return lane ? formatLaneProjectionLines(lane, theme, " ") : [];
565
+ }
566
+
567
+ function workflowPreflightLines(job: AsyncJobState, expanded = false): string[] {
568
+ if (job.mode !== "workflow" || !job.preflight) return [];
569
+ if (!expanded) {
570
+ const warning = formatWorkflowPreflightWarningSummary(job.workflow?.preflightWarnings, { indent: " ", hint: "expand for debug" });
571
+ return [
572
+ formatWorkflowPreflightPlanSummary(job.preflight, { indent: " " }),
573
+ ...(warning ? [warning] : []),
574
+ ];
575
+ }
576
+ return [
577
+ ...formatWorkflowPreflight(job.preflight, { indent: " " }).split("\n"),
578
+ ...(job.workflow?.preflightWarnings ? formatWorkflowPreflightWarnings(job.workflow.preflightWarnings, { indent: " " }).split("\n") : []),
579
+ ];
580
+ }
581
+
281
582
  function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
282
- const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
583
+ const flatIndex = foregroundResultIndex(details, resultIndex);
283
584
  const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
284
585
  for (const node of nodes) {
285
586
  if (node.flatIndex === flatIndex && node.label.trim()) return node.label;
@@ -291,6 +592,52 @@ function workflowLabelForResult(details: Details, resultIndex: number): string |
291
592
  return details.workflowGraph ? visit(details.workflowGraph.nodes) : undefined;
292
593
  }
293
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
+
294
641
  function hasLiveOutputSignal(line: string): boolean {
295
642
  const clean = oneLine(line);
296
643
  return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
@@ -474,7 +821,7 @@ function resultStatusLine(result: Details["results"][number], output: string): s
474
821
 
475
822
  type ResultPresentation = {
476
823
  glyph: string;
477
- label: "running" | "detached" | "stopped" | "paused" | "failed" | "completed";
824
+ label: "running" | "detached" | "stopped" | "paused" | "failed" | "partial" | "completed";
478
825
  tone: "accent" | "warning" | "error" | "success";
479
826
  };
480
827
 
@@ -484,6 +831,7 @@ function semanticResultPresentation(input: {
484
831
  stopped?: boolean;
485
832
  interrupted?: boolean;
486
833
  failed?: boolean;
834
+ partial?: boolean;
487
835
  completedWithoutOutput?: boolean;
488
836
  seed?: number;
489
837
  frame?: number;
@@ -496,6 +844,7 @@ function semanticResultPresentation(input: {
496
844
  if (input.stopped) return { glyph: "■", label: "stopped", tone: "warning" };
497
845
  if (input.interrupted) return { glyph: "■", label: "paused", tone: "warning" };
498
846
  if (input.failed) return { glyph: "✗", label: "failed", tone: "error" };
847
+ if (input.partial) return { glyph: "■", label: "partial", tone: "warning" };
499
848
  return { glyph: "✓", label: "completed", tone: input.completedWithoutOutput ? "warning" : "success" };
500
849
  }
501
850
 
@@ -574,6 +923,7 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
574
923
  return [
575
924
  step.index ?? index,
576
925
  step.agent,
926
+ step.sessionName,
577
927
  step.workflowKey,
578
928
  step.phase,
579
929
  step.label,
@@ -593,6 +943,23 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
593
943
  step.model,
594
944
  step.thinking,
595
945
  step.context,
946
+ step.description,
947
+ step.outputName,
948
+ step.structured,
949
+ step.acceptance?.status,
950
+ step.acceptance?.reviewResult?.status,
951
+ step.review?.status,
952
+ step.toolBudgetBlocked,
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,
962
+ step.watchdog?.phase,
596
963
  step.error,
597
964
  expanded ? expandedStepActivityRenderKey(step) : undefined,
598
965
  nestedRenderKey(step.children, expanded),
@@ -604,6 +971,7 @@ function nestedRenderKey(children: NestedRunSummary[] | undefined, expanded = fa
604
971
  child.id,
605
972
  child.state,
606
973
  child.agent,
974
+ child.sessionName,
607
975
  child.model,
608
976
  child.thinking,
609
977
  child.activityState,
@@ -623,6 +991,22 @@ function nestedRenderKey(children: NestedRunSummary[] | undefined, expanded = fa
623
991
  ]);
624
992
  }
625
993
 
994
+ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
995
+ return [
996
+ row.kind,
997
+ row.name,
998
+ row.state,
999
+ row.provider,
1000
+ row.role,
1001
+ row.verdict,
1002
+ row.reasonCode,
1003
+ row.detail,
1004
+ row.target,
1005
+ row.freshness,
1006
+ row.reportPath,
1007
+ ];
1008
+ }
1009
+
626
1010
  export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
627
1011
  return JSON.stringify({
628
1012
  asyncDir: job.asyncDir,
@@ -639,14 +1023,24 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
639
1023
  currentStep: job.currentStep,
640
1024
  chainStepCount: job.chainStepCount,
641
1025
  parallelGroups: job.parallelGroups,
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,
1031
+ preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
1032
+ preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
642
1033
  steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
643
1034
  nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
1035
+ lane: laneRenderKey(job),
644
1036
  stepsTotal: job.stepsTotal,
645
1037
  runningSteps: job.runningSteps,
646
1038
  completedSteps: job.completedSteps,
647
1039
  activeParallelGroup: job.activeParallelGroup,
648
1040
  startedAt: job.startedAt,
649
1041
  updatedAt: job.updatedAt,
1042
+ timedOut: job.timedOut,
1043
+ stopped: job.stopped,
650
1044
  totalTokens: job.totalTokens,
651
1045
  });
652
1046
  }
@@ -666,6 +1060,65 @@ function widgetJobName(job: AsyncJobState): string {
666
1060
  return job.mode ?? "subagent";
667
1061
  }
668
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
+
669
1122
  function widgetActivity(job: AsyncJobState): string {
670
1123
  const facts: string[] = [];
671
1124
  if (job.currentTool && job.currentToolStartedAt !== undefined && job.updatedAt !== undefined) facts.push(`${job.currentTool} ${formatDuration(Math.max(0, job.updatedAt - job.currentToolStartedAt))}`);
@@ -681,6 +1134,7 @@ function widgetActivity(job: AsyncJobState): string {
681
1134
  if (job.status === "queued") return "queued…";
682
1135
  if (job.status === "paused") return "Paused";
683
1136
  if (job.status === "stopped") return "Stopped";
1137
+ if (job.status === "partial") return "Partial";
684
1138
  if (job.status === "failed") return "Failed";
685
1139
  return "Done";
686
1140
  }
@@ -773,8 +1227,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
773
1227
  for (const span of buildAsyncChainStepSpans(total, job.steps.length, job.parallelGroups)) {
774
1228
  const steps = job.steps.slice(span.start, span.start + span.count);
775
1229
  if (span.isParallel) {
776
- const status = aggregateStepStatus(steps);
777
- 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));
778
1231
  continue;
779
1232
  }
780
1233
  const step = steps[0];
@@ -791,6 +1244,8 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
791
1244
  if (!job.steps?.length) return [];
792
1245
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
793
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));
794
1249
  const total = job.stepsTotal ?? job.steps.length;
795
1250
  const lines: string[] = [];
796
1251
  for (const [index, step] of job.steps.entries()) {
@@ -799,8 +1254,10 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
799
1254
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
800
1255
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
801
1256
  const label = compactTaskText(step.description, step.label);
802
- const display = label ? `${label} (${step.agent})` : step.agent;
1257
+ const display = step.sessionName?.trim() || (label ? `${label} (${step.agent})` : step.agent);
803
1258
  lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${display} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
1259
+ const lane = projectAsyncLane(job, step);
1260
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
804
1261
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
805
1262
  }
806
1263
  return lines;
@@ -879,6 +1336,127 @@ function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGrou
879
1336
  return spans;
880
1337
  }
881
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
+
882
1460
  function isDoneResult(result: Details["results"][number]): boolean {
883
1461
  const status = result.progress?.status;
884
1462
  if (status === "completed") return true;
@@ -897,41 +1475,83 @@ interface ChainRenderResultEntry {
897
1475
  rowNumber: number;
898
1476
  rowLabel?: string;
899
1477
  agentName: string;
1478
+ displayIndex: number;
1479
+ isParallel?: boolean;
900
1480
  }
901
1481
 
902
- interface ChainRenderPlaceholderEntry {
903
- kind: "placeholder";
904
- rowNumber: number;
1482
+ interface ChainRenderGroupEntry {
1483
+ kind: "group";
905
1484
  stepLabel: string;
906
- agentName: string;
1485
+ groupLabel?: string;
907
1486
  status: WorkflowNodeStatus;
908
1487
  error?: string;
909
1488
  }
910
1489
 
911
- 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
+ }
912
1514
 
913
1515
  function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
914
1516
  if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
915
1517
  const entries: ChainRenderEntry[] = [];
916
1518
  for (const span of buildChainStepSpans(details)) {
917
- if (span.isParallel && span.count === 0) {
1519
+ if (span.isParallel) {
918
1520
  entries.push({
919
- kind: "placeholder",
920
- rowNumber: span.stepIndex + 1,
921
- stepLabel: `Step ${span.stepIndex + 1}`,
922
- agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
923
- 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),
924
1525
  error: span.error,
925
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));
926
1544
  continue;
927
1545
  }
928
1546
  for (let index = span.start; index < span.start + span.count; index++) {
1547
+ const result = details.results[index];
929
1548
  entries.push({
930
1549
  kind: "result",
931
1550
  resultIndex: index,
932
- rowNumber: index + 1,
933
- rowLabel: span.isParallel ? `Agent ${index - span.start + 1}/${span.count}` : `Step ${span.stepIndex + 1}`,
934
- agentName: details.results[index]?.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),
935
1555
  });
936
1556
  }
937
1557
  }
@@ -947,6 +1567,7 @@ interface MultiProgressLabel {
947
1567
  groupStartIndex: number;
948
1568
  groupEndIndex: number;
949
1569
  showActiveGroupOnly: boolean;
1570
+ logicalStepCount: number;
950
1571
  }
951
1572
 
952
1573
  function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
@@ -982,7 +1603,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
982
1603
  const headerLabel = hasRunning
983
1604
  ? `${formatAgentRunningLabel(running)} · ${done}/${totalCount} done`
984
1605
  : `${done}/${totalCount} done`;
985
- 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 };
986
1607
  }
987
1608
 
988
1609
  if (activeParallelGroup) {
@@ -1010,7 +1631,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
1010
1631
  const headerLabel = hasRunning
1011
1632
  ? `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${formatAgentRunningLabel(running)} · ${done}/${groupSize} done`
1012
1633
  : `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${done}/${groupSize} done`;
1013
- 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 };
1014
1635
  }
1015
1636
 
1016
1637
  if (details.mode === "chain" && details.chainAgents?.length) {
@@ -1028,30 +1649,66 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
1028
1649
  }).length;
1029
1650
  const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, doneLogical + (hasRunning ? 1 : 0));
1030
1651
  const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${doneLogical}/${totalCount}`;
1031
- 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 };
1032
1653
  }
1033
1654
 
1034
1655
  const totalCount = details.totalSteps ?? details.results.length;
1035
1656
  const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, details.results.filter(isDoneResult).length + (hasRunning ? 1 : 0));
1036
1657
  const done = details.results.filter(isDoneResult).length;
1037
1658
  const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${done}/${totalCount}`;
1038
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
1039
- }
1040
-
1041
- function resultRowLabel(label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
1042
- if (label.itemTitle === "Agent") {
1043
- const localStepNumber = label.activeParallelGroup
1044
- ? resultIndex - label.groupStartIndex + 1
1045
- : stepNumber;
1046
- return `Agent ${localStepNumber}/${label.totalCount}`;
1047
- }
1048
- 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;
1049
1697
  }
1050
1698
 
1051
1699
  function widgetStats(job: AsyncJobState, theme: Theme): string {
1052
1700
  const parts: string[] = [];
1053
- const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
1054
- 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) {
1055
1712
  const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
1056
1713
  const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
1057
1714
  if (job.mode === "parallel") {
@@ -1068,10 +1725,10 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
1068
1725
  parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`);
1069
1726
  }
1070
1727
  } else if (job.currentStep !== undefined) {
1071
- if (job.mode === "chain" && job.parallelGroups?.length) {
1728
+ if (job.mode === "chain") {
1072
1729
  const total = job.chainStepCount ?? stepsTotal;
1073
- parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`);
1074
- } else {
1730
+ parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups ?? []) + 1}/${total}`);
1731
+ } else if (!isSingleChild) {
1075
1732
  parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
1076
1733
  }
1077
1734
  } else if (stepsTotal > 1) {
@@ -1116,6 +1773,7 @@ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["s
1116
1773
  }
1117
1774
 
1118
1775
  function nestedRunName(run: NestedRunSummary): string {
1776
+ if (run.sessionName?.trim()) return run.sessionName.trim();
1119
1777
  if (run.agent) return run.agent;
1120
1778
  if (run.agents?.length) return formatWidgetAgents(run.agents);
1121
1779
  return run.id;
@@ -1125,6 +1783,7 @@ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary[
1125
1783
  if (state === "running") return theme.fg("accent", runningGlyph(seed));
1126
1784
  if (state === "complete" || state === "completed") return theme.fg("success", "✓");
1127
1785
  if (state === "failed") return theme.fg("error", "✗");
1786
+ if (state === "partial") return theme.fg("warning", "■");
1128
1787
  if (state === "paused") return theme.fg("warning", "■");
1129
1788
  if (state === "stopped") return theme.fg("warning", "■");
1130
1789
  return theme.fg("muted", "◦");
@@ -1172,6 +1831,7 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
1172
1831
  if (state === "queued" || state === "pending") return "queued…";
1173
1832
  if (state === "paused") return "Paused";
1174
1833
  if (state === "stopped") return "Stopped";
1834
+ if (state === "partial") return "Partial";
1175
1835
  if (state === "failed") return "Failed";
1176
1836
  return "Done";
1177
1837
  }
@@ -1196,7 +1856,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
1196
1856
  const activity = nestedActivity(step, state, snapshotNow ?? fallback);
1197
1857
  const timestamp = "status" in step ? nestedStepTimestamp(step, fallback) : formatClockTime(nestedRunEventTime(step));
1198
1858
  const error = step.error ? ` · ${step.error}` : "";
1199
- const name = "status" in step ? step.agent : nestedRunName(step);
1859
+ const name = "status" in step ? childDisplayName(step) : nestedRunName(step);
1200
1860
  rows.push({
1201
1861
  prefix,
1202
1862
  text: `${nestedTimestampPrefix(timestamp)}${nestedStatusGlyph(state, theme)} ${name} · ${state}${modelThinking ? ` · ${modelThinking}` : ""}${activity ? ` · ${activity}` : ""}${error}`,
@@ -1258,7 +1918,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
1258
1918
  for (const step of child.steps ?? []) {
1259
1919
  if (lines.length >= lineBudget) return;
1260
1920
  const modelThinking = formatModelThinking(step.model, step.thinking);
1261
- lines.push(theme.fg("dim", `${prefix} ↳ ${nestedTimestampPrefix(nestedStepTimestamp(step, child.lastUpdate))}${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status}${modelThinking ? ` · ${modelThinking}` : ""} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
1921
+ lines.push(theme.fg("dim", `${prefix} ↳ ${nestedTimestampPrefix(nestedStepTimestamp(step, child.lastUpdate))}${nestedStatusGlyph(step.status, theme)} ${childDisplayName(step)} · ${step.status}${modelThinking ? ` · ${modelThinking}` : ""} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
1262
1922
  append(step.children, depth + 1, `${prefix} `);
1263
1923
  }
1264
1924
  append(child.children, depth + 1, `${prefix} `);
@@ -1272,57 +1932,126 @@ function foregroundStyleWidgetStepLines(
1272
1932
  job: AsyncJobState,
1273
1933
  theme: Theme,
1274
1934
  step: NonNullable<AsyncJobState["steps"]>[number],
1275
- itemTitle: "Agent" | "Step",
1935
+ itemTitle: "Agent" | "Stage" | "Step",
1276
1936
  index: number,
1277
1937
  total: number,
1278
1938
  expanded: boolean,
1279
1939
  width: number,
1280
1940
  frame?: number,
1941
+ options?: { rowLabel?: string; rowIndent?: string; detailIndent?: string; rowMarker?: string; includeCurrentPath?: boolean },
1281
1942
  ): string[] {
1943
+ const rowIndent = options?.rowIndent ?? " ";
1944
+ const detailIndent = options?.detailIndent ?? " ";
1282
1945
  const status = widgetStepStatus(step.status, theme);
1283
1946
  const stats = widgetStepStats(theme, step);
1284
1947
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1285
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1286
- const task = compactTaskText(step.description, step.label);
1287
- if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
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}` : ""}`];
1957
+ const lane = projectAsyncLane(job, step);
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}`)}`);
1288
1961
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
1289
- 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}`)}`);
1290
1967
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
1291
- lines.push(` ${nestedLine}`);
1968
+ lines.push(`${detailIndent}${nestedLine}`);
1292
1969
  }
1970
+ const error = step.error?.trim() || step.execution?.error?.trim();
1971
+ if (error) lines.push(`${detailIndent}${theme.fg("error", `error: ${oneLine(error)}`)}`);
1293
1972
  if (step.status === "running") {
1294
- if (!expanded) lines.push(` ${theme.fg("accent", liveDetailHintText())}`);
1973
+ if (!expanded) lines.push(`${detailIndent}${theme.fg("accent", liveDetailHintText())}`);
1295
1974
  const output = widgetOutputPath(job, step);
1296
- if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
1975
+ if (output) lines.push(`${detailIndent}${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
1297
1976
  if (expanded) {
1298
1977
  const liveStatus = buildLiveStatusLine(step, job.updatedAt);
1299
- if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
1978
+ if (liveStatus && liveStatus !== activity) lines.push(`${detailIndent}${theme.fg("accent", liveStatus)}`);
1300
1979
  for (const tool of step.recentTools?.slice(-3) ?? []) {
1301
1980
  const maxArgsLen = Math.max(40, width - 30);
1302
1981
  const argsPreview = renderToolArgsPreview(tool.args, maxArgsLen, expanded);
1303
- lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
1982
+ lines.push(`${detailIndent} ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
1304
1983
  }
1305
1984
  for (const line of compactRecentOutputLines(step.recentOutput)) {
1306
- lines.push(` ${theme.fg("dim", line)}`);
1985
+ lines.push(`${detailIndent} ${theme.fg("dim", line)}`);
1307
1986
  }
1308
1987
  }
1309
1988
  }
1310
1989
  return lines;
1311
1990
  }
1312
1991
 
1992
+ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string): string[] {
1993
+ const rows = projectAsyncWorkflowRows([], job.hostSteps).filter((row) => row.kind);
1994
+ const visible = rows.slice(0, 8);
1995
+ const lines = visible.map((row) => {
1996
+ const state = hostStepVerdictLabel(row.state as HostStepState, row.verdict as HostStepVerdict | undefined);
1997
+ const glyph = state === "running" ? theme.fg("accent", "●")
1998
+ : state === "pending" ? theme.fg("muted", "◦")
1999
+ : state === "pass" ? theme.fg("success", "✓")
2000
+ : state === "fail" || state === "error" ? theme.fg("error", "✗")
2001
+ : theme.fg("warning", "■");
2002
+ const details = [
2003
+ row.provider ? `provider:${row.provider}` : undefined,
2004
+ row.role ? `role:${row.role}` : undefined,
2005
+ row.target,
2006
+ row.detail,
2007
+ row.reasonCode ? `reason:${row.reasonCode}` : undefined,
2008
+ row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : undefined,
2009
+ row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : undefined,
2010
+ ].filter(Boolean).join(" · ");
2011
+ return `${indent}${glyph} ${row.kind}: ${row.name} · ${state}${details ? ` · ${details}` : ""}`;
2012
+ });
2013
+ if (rows.length > visible.length) lines.push(`${indent}${theme.fg("dim", `… +${rows.length - visible.length} host steps hidden`)}`);
2014
+ return lines;
2015
+ }
2016
+
1313
2017
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
1314
- if (!job.steps?.length) return [
1315
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1316
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
1317
- ];
2018
+ const steps = workflowWidgetSteps(job);
2019
+ if (!steps.length) {
2020
+ const lane = projectAsyncLane(job);
2021
+ return [
2022
+ ...workflowPreflightLines(job, expanded),
2023
+ ...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
2024
+ ...hostStepWidgetLines(job, theme, " "),
2025
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2026
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
2027
+ ];
2028
+ }
1318
2029
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
1319
- const total = job.stepsTotal ?? job.steps.length;
1320
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
1321
- const lines: string[] = [];
1322
- for (const [index, step] of job.steps.entries()) {
1323
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, frame));
2030
+ const lines: string[] = workflowPreflightLines(job, expanded);
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
+ }
1324
2052
  }
1325
- const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
2053
+ lines.push(...hostStepWidgetLines(job, theme, " "));
2054
+ const attached = new Set(steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
1326
2055
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
1327
2056
  for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
1328
2057
  lines.push(` ${nestedLine}`);
@@ -1332,12 +2061,16 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
1332
2061
 
1333
2062
  function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
1334
2063
  const stats = widgetStats(job, theme);
1335
- 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;
1336
2065
  const mode = widgetJobName(job);
1337
- 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}` : ""}`;
1338
2071
  return [
1339
2072
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
1340
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
2073
+ ...(collapseDetails ? [] : [summary]),
1341
2074
  ...foregroundStyleWidgetDetails(job, theme, expanded, width, frame),
1342
2075
  ].map((line) => truncLine(line, width));
1343
2076
  }
@@ -1346,10 +2079,13 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1346
2079
  const fullLines = buildSingleWidgetLines(job, theme, width, false, frame);
1347
2080
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
1348
2081
 
1349
- const total = job.stepsTotal ?? job.steps.length;
1350
- 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));
1351
2085
  const lines = fullLines.slice(0, 2);
1352
- 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;
1353
2089
  const status = widgetStepStatus(step.status, theme);
1354
2090
  const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
1355
2091
  const stepStats = widgetStepStats(theme, step);
@@ -1357,9 +2093,13 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1357
2093
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1358
2094
  const task = compactTaskText(step.description, step.label);
1359
2095
  const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
1360
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1361
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
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}` : ""}`);
2098
+ const lane = projectAsyncLane(job, step);
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}`);
1362
2101
  }
2102
+ lines.push(...hostStepWidgetLines(job, theme, " "));
1363
2103
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
1364
2104
  return lines.map((line) => truncLine(line, width));
1365
2105
  }
@@ -1496,9 +2236,18 @@ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, fra
1496
2236
  const stats = widgetStats(job, theme);
1497
2237
  const activity = widgetActivity(job);
1498
2238
  const status = job.status === "complete" ? "done" : job.status;
2239
+ const lane = projectAsyncLane(job);
2240
+ const laneSummary = lane
2241
+ ? [lane.label ?? lane.role, lane.phase ? `phase:${lane.phase}` : undefined, lane.output ? `out:${lane.output}` : undefined, lane.workspace ? `workspace:${lane.workspace}` : `ref:${lane.ref}`].filter(Boolean).join(" · ")
2242
+ : "";
2243
+ const laneSignals = lane
2244
+ ? [lane.next ? `next:${lane.next}` : undefined, ...lane.chips.map((chip) => formatLaneChip(chip, theme))].filter(Boolean).join(" · ")
2245
+ : "";
1499
2246
  const parts = [
1500
2247
  `${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}`,
1501
2248
  theme.fg("dim", status),
2249
+ laneSignals ? laneSignals : "",
2250
+ laneSummary ? theme.fg("dim", laneSummary) : "",
1502
2251
  stats,
1503
2252
  activity && activity.toLowerCase() !== status ? theme.fg("dim", activity) : "",
1504
2253
  ].filter(Boolean);
@@ -1591,6 +2340,10 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
1591
2340
  widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
1592
2341
  return fitWidgetLineBudget(lines, theme, width, false);
1593
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
+ }
1594
2347
 
1595
2348
  if (availableRows <= 2) {
1596
2349
  widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
@@ -1637,15 +2390,19 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1637
2390
  let hiddenFinished = 0;
1638
2391
  let queuedSummaryShown = false;
1639
2392
  let slots = MAX_WIDGET_JOBS;
1640
-
1641
- for (const job of running) {
1642
- if (slots <= 0) { hiddenRunning++; continue; }
2393
+ const appendJob = (job: AsyncJobState): void => {
1643
2394
  const stats = widgetStats(job, theme);
1644
2395
  items.push([
1645
2396
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1646
2397
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
2398
+ ...widgetLaneDetailLines(job, theme),
1647
2399
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1648
2400
  ]);
2401
+ };
2402
+
2403
+ for (const job of running) {
2404
+ if (slots <= 0) { hiddenRunning++; continue; }
2405
+ appendJob(job);
1649
2406
  slots--;
1650
2407
  }
1651
2408
 
@@ -1657,12 +2414,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1657
2414
 
1658
2415
  for (const job of finished) {
1659
2416
  if (slots <= 0) { hiddenFinished++; continue; }
1660
- const stats = widgetStats(job, theme);
1661
- items.push([
1662
- `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1663
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1664
- ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1665
- ]);
2417
+ appendJob(job);
1666
2418
  slots--;
1667
2419
  }
1668
2420
 
@@ -1728,7 +2480,7 @@ function renderSingleCompact(
1728
2480
  const detailIndent = mainWindowIndent(layout, 1);
1729
2481
  const continuationIndent = mainWindowIndent(layout, 2) + (layout.horizontalSpacing > 0 ? " " : "");
1730
2482
  const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1731
- c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${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));
1732
2484
 
1733
2485
  if (isRunning && r.progress) {
1734
2486
  const task = compactTaskText(r.task);
@@ -1761,6 +2513,7 @@ function renderSingleCompact(
1761
2513
  }
1762
2514
 
1763
2515
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
2516
+ if (row.state === "planned") return theme.fg("muted", "◦");
1764
2517
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1765
2518
  if (row.state === "complete") return theme.fg("success", "✓");
1766
2519
  if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", "■");
@@ -1769,6 +2522,7 @@ function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: nu
1769
2522
 
1770
2523
  function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): string {
1771
2524
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
2525
+ if (row.state === "planned") return theme.fg("dim", label);
1772
2526
  if (row.state === "running") return theme.fg("accent", label);
1773
2527
  if (row.state === "complete") return theme.fg("success", label);
1774
2528
  if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", label);
@@ -1783,9 +2537,9 @@ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue:
1783
2537
  return "running";
1784
2538
  }
1785
2539
 
1786
- function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
2540
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number, expanded = false): Component {
1787
2541
  const workflow = d.workflow;
1788
- const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
2542
+ const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace, d.preflight) : d.preflight ? buildWorkflowChatProgressRows([], d.preflight) : [];
1789
2543
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1790
2544
  const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : state === "paused" ? theme.fg("warning", "■") : theme.fg("error", "✗");
1791
2545
  const width = getTermWidth() - 4;
@@ -1796,6 +2550,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1796
2550
  const rowIndent = mainWindowIndent(layout, 1);
1797
2551
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold("workflow"))} ${runId} ${theme.fg("dim", "·")} ${d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo"} ${theme.fg("dim", "·")} ${state}`, width), 0, 0));
1798
2552
  c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Repo ${repoLabel}`), width), 0, 0));
2553
+ if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
1799
2554
  if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
1800
2555
  if (rows.length === 0) {
1801
2556
  c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ waiting for workflow child launches`), width), 0, 0));
@@ -1809,7 +2564,20 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1809
2564
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1810
2565
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1811
2566
  const error = row.error ? ` ${theme.fg(row.state === "detached" ? "warning" : "error", `· ${compactWorkflowError(row.error)}`)}` : "";
1812
- c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
2567
+ const hints = expanded && row.preflight ? [
2568
+ row.preflight.mode ? `mode:${row.preflight.mode}` : undefined,
2569
+ row.preflight.decision ? `decision:${row.preflight.decision}` : undefined,
2570
+ row.preflight.claims?.length ? `claims:${row.preflight.claims.join(",")}` : undefined,
2571
+ row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
2572
+ row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
2573
+ ].filter((value): value is string => Boolean(value)).join(" · ") : "";
2574
+ c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}${hints ? ` ${theme.fg("dim", `· ${hints}`)}` : ""}`, width), 0, 0));
2575
+ }
2576
+ if (workflow?.preflightWarnings?.length) {
2577
+ const warningLines = expanded
2578
+ ? formatWorkflowPreflightWarnings(workflow.preflightWarnings, { indent: rowIndent }).split("\n")
2579
+ : [formatWorkflowPreflightWarningSummary(workflow.preflightWarnings, { indent: rowIndent, hint: "expand for debug" })];
2580
+ for (const warningLine of warningLines) c.addChild(new Text(truncLine(theme.fg("warning", warningLine), width), 0, 0));
1813
2581
  }
1814
2582
  if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
1815
2583
  return c;
@@ -1825,6 +2593,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1825
2593
  || workflowGraphHasStatus(d, ["failed"]);
1826
2594
  const paused = d.results.some((r) => r.interrupted)
1827
2595
  || workflowGraphHasStatus(d, ["paused"]);
2596
+ const partial = workflowGraphHasStatus(d, ["partial"]);
1828
2597
  let totalSummary = d.progressSummary;
1829
2598
  if (!totalSummary) {
1830
2599
  let sawProgress = false;
@@ -1848,6 +2617,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1848
2617
  stopped,
1849
2618
  interrupted: paused,
1850
2619
  failed,
2620
+ partial,
1851
2621
  seed: runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex),
1852
2622
  frame,
1853
2623
  });
@@ -1857,25 +2627,19 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1857
2627
  const width = getTermWidth() - 4;
1858
2628
  const rowIndent = mainWindowIndent(layout, 1);
1859
2629
  const detailIndent = mainWindowIndent(layout, 2);
1860
- const continuationIndent = mainWindowIndent(layout, 3) + (layout.horizontalSpacing > 0 ? " " : "");
1861
2630
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1862
2631
 
1863
2632
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
1864
2633
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
1865
2634
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
1866
2635
  const chainEntries = buildChainRenderEntries(d, multiLabel);
1867
- const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
1868
- const i = displayStart + offset;
1869
- const r = d.results[i];
1870
- const fallbackLabel = itemTitle.toLowerCase();
1871
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1872
- return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
1873
- });
2636
+ const renderEntries = chainEntries ?? buildForegroundResultEntries(d, multiLabel, displayStart, displayEnd, useResultsDirectly);
1874
2637
  for (const entry of renderEntries) {
1875
- if (entry.kind === "placeholder") {
2638
+ if (entry.kind === "group") {
1876
2639
  const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
1877
2640
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1878
- 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));
1879
2643
  if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", `${detailIndent}⎿ Error: ${entry.error}`), width), 0, 0));
1880
2644
  continue;
1881
2645
  }
@@ -1884,23 +2648,24 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1884
2648
  const rowNumber = entry.rowNumber;
1885
2649
  const agentName = entry.agentName;
1886
2650
  if (!r) {
1887
- const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
1888
- 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));
1889
2654
  continue;
1890
2655
  }
1891
2656
  const output = getSingleResultOutput(r);
1892
- 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);
1893
2658
  const rProg = r.progress || progressFromArray || r.progressSummary;
1894
2659
  const rRunning = rProg && "status" in rProg && isResultRunning(r, rProg.status);
1895
2660
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
1896
- const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
1897
2661
  const stepStats = formatProgressStats(theme, rProg);
1898
2662
  const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
1899
2663
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1900
- const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
2664
+ const stepLabel = entry.rowLabel;
1901
2665
  const rowProgressModel = rProg && "status" in rProg ? rProg : undefined;
1902
2666
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1903
- 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}`;
1904
2669
  c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
1905
2670
  if (rRunning || rPending) {
1906
2671
  const task = compactTaskText(r.task, workflowLabelForResult(d, i));
@@ -1951,7 +2716,8 @@ export function renderSubagentSummary(
1951
2716
  const failed = result.isError === true
1952
2717
  || results.some((entry) => !hasTerminalResultFlag(entry) && entry.exitCode !== 0 && !isResultRunning(entry))
1953
2718
  || Boolean(details && workflowGraphHasStatus(details, ["failed"]));
1954
- const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : "completed";
2719
+ const partial = Boolean(details && workflowGraphHasStatus(details, ["partial"]));
2720
+ const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : partial ? "partial" : "completed";
1955
2721
  const glyph = state === "running"
1956
2722
  ? theme.fg("accent", STATIC_RUNNING_GLYPH)
1957
2723
  : state === "completed"
@@ -1960,7 +2726,7 @@ export function renderSubagentSummary(
1960
2726
  ? theme.fg("error", "✗")
1961
2727
  : theme.fg("warning", "■");
1962
2728
  const label = details?.mode === "single" && results.length === 1
1963
- ? results[0]?.agent || "subagent"
2729
+ ? foregroundSingleDisplayName(results[0])
1964
2730
  : details?.mode || "subagent";
1965
2731
  return new Text(
1966
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),
@@ -1984,7 +2750,7 @@ export function renderSubagentResult(
1984
2750
  const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
1985
2751
  const d = result.details;
1986
2752
  if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && d.workflow?.value === undefined && (!result.isError || (d.workflow?.trace.length ?? 0) > 0)) {
1987
- return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
2753
+ return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame, options.expanded));
1988
2754
  }
1989
2755
  if (!d || !d.results.length) {
1990
2756
  const t = result.content[0];
@@ -1995,9 +2761,12 @@ export function renderSubagentResult(
1995
2761
  if (d && !options.expanded && !result.isError) {
1996
2762
  const lines = text.split(/\r?\n/);
1997
2763
  const firstNonEmptyLine = lines.find((line) => line.trim())?.trim() || "(no output)";
2764
+ const compactLine = d.mode === "workflow" && d.preflight
2765
+ ? formatWorkflowPreflightPlanSummary(d.preflight)
2766
+ : firstNonEmptyLine;
1998
2767
  const c = new Container();
1999
2768
  const detailIndent = mainWindowIndent(layout, 1);
2000
- c.addChild(new Text(truncLine(`${contextPrefix}${firstNonEmptyLine} · ${lines.length} lines`, width), 0, 0));
2769
+ c.addChild(new Text(truncLine(`${contextPrefix}${compactLine} · ${lines.length} lines`, width), 0, 0));
2001
2770
  c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}Press ${liveDetailKeyText()} for full output`), width), 0, 0));
2002
2771
  return compact(c);
2003
2772
  }
@@ -2030,7 +2799,7 @@ export function renderSubagentResult(
2030
2799
  const fit = (text: string) => expanded ? text : truncLine(text, w);
2031
2800
  const toolCallLines = getToolCallLines(r, expanded);
2032
2801
  const c = new Container();
2033
- c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(r.agent))}${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));
2034
2803
  c.addChild(new Spacer(1));
2035
2804
  const taskMaxLen = Math.max(20, w - 8);
2036
2805
  const taskPreview = expanded || r.task.length <= taskMaxLen
@@ -2040,6 +2809,9 @@ export function renderSubagentResult(
2040
2809
  new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
2041
2810
  );
2042
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
+ }
2043
2815
 
2044
2816
  if (isRunning && r.progress) {
2045
2817
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
@@ -2118,6 +2890,7 @@ export function renderSubagentResult(
2118
2890
  || workflowGraphHasStatus(d, ["failed"]);
2119
2891
  const paused = d.results.some((r) => r.interrupted)
2120
2892
  || workflowGraphHasStatus(d, ["paused"]);
2893
+ const partial = workflowGraphHasStatus(d, ["partial"]);
2121
2894
  const completedWithoutOutput = d.results.some((r) =>
2122
2895
  !hasTerminalResultFlag(r)
2123
2896
  && r.exitCode === 0
@@ -2130,6 +2903,7 @@ export function renderSubagentResult(
2130
2903
  stopped,
2131
2904
  interrupted: paused,
2132
2905
  failed,
2906
+ partial,
2133
2907
  completedWithoutOutput,
2134
2908
  frame,
2135
2909
  }), theme);
@@ -2169,6 +2943,7 @@ export function renderSubagentResult(
2169
2943
  ? d.chainAgents
2170
2944
  .map((agent, i) => {
2171
2945
  const result = d.results[i];
2946
+ const displayName = foregroundResultDisplayName(d, i, result, agent);
2172
2947
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
2173
2948
  const stepPresentation = result
2174
2949
  ? styledResultPresentation(resultPresentation(result, getSingleResultOutput(result), isCurrent && hasRunning && !hasTerminalResultFlag(result)), theme)
@@ -2176,7 +2951,7 @@ export function renderSubagentResult(
2176
2951
  const stepStatus = stepPresentation
2177
2952
  ? `${stepPresentation.glyph} ${stepPresentation.label}`
2178
2953
  : theme.fg("dim", "◦ pending");
2179
- return `${stepStatus} ${agent}${contextModeBadge(theme, result?.context)}`;
2954
+ return `${stepStatus} ${displayName}${contextModeBadge(theme, result?.context)}`;
2180
2955
  })
2181
2956
  .join(theme.fg("dim", " → "))
2182
2957
  : null;
@@ -2199,19 +2974,15 @@ export function renderSubagentResult(
2199
2974
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
2200
2975
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
2201
2976
  const chainEntries = buildChainRenderEntries(d, multiLabel);
2202
- const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
2203
- const i = displayStart + offset;
2204
- const r = d.results[i];
2205
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
2206
- return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
2207
- });
2977
+ const renderEntries = chainEntries ?? buildForegroundResultEntries(d, multiLabel, displayStart, displayEnd, useResultsDirectly);
2208
2978
 
2209
2979
  c.addChild(new Spacer(1));
2210
2980
 
2211
2981
  for (const entry of renderEntries) {
2212
- if (entry.kind === "placeholder") {
2982
+ if (entry.kind === "group") {
2213
2983
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
2214
- 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));
2215
2986
  c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
2216
2987
  if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
2217
2988
  c.addChild(new Spacer(1));
@@ -2223,28 +2994,28 @@ export function renderSubagentResult(
2223
2994
  const agentName = entry.agentName;
2224
2995
 
2225
2996
  if (!r) {
2226
- const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
2227
- 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));
2228
3000
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
2229
3001
  c.addChild(new Spacer(1));
2230
3002
  continue;
2231
3003
  }
2232
3004
 
2233
- const progressFromArray = d.progress?.find((p) => p.index === i)
2234
- || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
3005
+ const progressFromArray = foregroundProgressForResult(d, i);
2235
3006
  const rProg = r.progress || progressFromArray || r.progressSummary;
2236
3007
  const rRunning = isResultRunning(r, rProg?.status);
2237
- const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
2238
3008
 
2239
3009
  const resultOutput = getSingleResultOutput(r);
2240
3010
  const rowPresentation = styledResultPresentation(resultPresentation(r, resultOutput, rRunning, progressRunningSeed(rProg), frame), theme);
2241
3011
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
2242
3012
  const modelDisplay = modelThinkingBadge(theme, r.model ?? rProg?.model, r.thinking ?? rProg?.thinking);
2243
- const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
3013
+ const stepLabel = entry.rowLabel;
2244
3014
  const contextBadge = contextModeBadge(theme, r.context);
3015
+ const labelPrefix = stepLabel ? `${stepLabel}: ` : "";
2245
3016
  const stepHeader = rRunning
2246
- ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
2247
- : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(r.agent)}${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}`;
2248
3019
  const toolCallLines = getToolCallLines(r, expanded);
2249
3020
  c.addChild(new Text(fit(stepHeader), 0, 0));
2250
3021
 
@@ -2258,6 +3029,9 @@ export function renderSubagentResult(
2258
3029
  if (outputTarget) {
2259
3030
  c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
2260
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
+ }
2261
3035
 
2262
3036
  if (r.skills?.length) {
2263
3037
  c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));