pi-subagents 0.57.0 → 0.59.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 (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  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 +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  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/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. 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,
@@ -23,14 +25,17 @@ import {
23
25
  } from "../shared/types.ts";
24
26
  import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
25
27
  import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
26
- import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
28
+ import { formatContextUsage, formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
27
29
  import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
28
30
  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";
32
34
  import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
35
+ import { formatWorkflowPreflight, formatWorkflowPreflightPlanSummary, formatWorkflowPreflightWarningSummary, formatWorkflowPreflightWarnings } from "../workflows/workflow-preflight.ts";
33
36
  import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status-snapshot.ts";
37
+ import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
38
+ import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
34
39
 
35
40
  type Theme = ExtensionContext["ui"]["theme"];
36
41
 
@@ -266,14 +271,188 @@ function oneLine(text: string): string {
266
271
 
267
272
  const COMPACT_TASK_MAX_CHARS = 96;
268
273
 
274
+ /** Display label for a child run: the derived session name (agent + task
275
+ * excerpt) when the launcher provided one, else the bare agent name. */
276
+ function childDisplayName(result: { agent?: string; sessionName?: string } | undefined, fallback = "subagent"): string {
277
+ return result?.sessionName?.trim() || result?.agent || fallback;
278
+ }
279
+
269
280
  export function compactTaskText(task: string | undefined, label?: string): string | undefined {
270
- const source = label?.trim() || task?.trim();
271
- if (!source || source === PROMPT_REDACTED) return undefined;
272
- const normalized = oneLine(source);
281
+ const taskText = task?.trim();
282
+ const labelText = label?.trim();
283
+ const normalizedTask = taskText && taskText !== PROMPT_REDACTED ? oneLine(taskText) : "";
284
+ const normalizedLabel = labelText && labelText !== PROMPT_REDACTED ? oneLine(labelText) : "";
285
+ const normalized = normalizedLabel && normalizedTask && normalizedLabel !== normalizedTask
286
+ ? `${normalizedLabel} — ${normalizedTask}`
287
+ : normalizedLabel || normalizedTask;
273
288
  if (!normalized) return undefined;
274
289
  return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
275
290
  }
276
291
 
292
+ export interface AsyncLaneProjection {
293
+ label?: string;
294
+ role: string;
295
+ phase?: string;
296
+ state: AsyncJobState["status"] | AsyncJobStep["status"];
297
+ gate?: string;
298
+ next?: string;
299
+ output?: string;
300
+ workspace?: string;
301
+ ref: string;
302
+ chips: string[];
303
+ }
304
+
305
+ const LANE_VALUE_MAX_CHARS = 48;
306
+
307
+ function boundedLaneValue(value: string | undefined, maxChars = LANE_VALUE_MAX_CHARS): string | undefined {
308
+ if (!value?.trim() || value.trim() === PROMPT_REDACTED) return undefined;
309
+ return previewDisplayText(oneLine(value), maxChars);
310
+ }
311
+
312
+ function laneStepForJob(job: AsyncJobState): AsyncJobStep | undefined {
313
+ const steps = job.steps ?? [];
314
+ if (steps.length === 0) return undefined;
315
+ if (job.currentStep !== undefined) {
316
+ const current = steps[job.currentStep];
317
+ if (current && (current.index === undefined || current.index === job.currentStep)) return current;
318
+ // Active parallel groups retain flat indices while exposing only the group slice.
319
+ const indexedCurrent = steps.find((step) => step.index === job.currentStep);
320
+ if (indexedCurrent) return indexedCurrent;
321
+ return steps[0];
322
+ }
323
+ return steps.find((step) => step.status === "running")
324
+ ?? steps.find((step) => step.status === "pending")
325
+ ?? steps.at(-1);
326
+ }
327
+
328
+ function laneTraceForJob(job: AsyncJobState): NonNullable<AsyncJobState["workflow"]>["trace"][number] | undefined {
329
+ return Array.isArray(job.workflow?.trace) ? job.workflow.trace.at(-1) : undefined;
330
+ }
331
+
332
+ function laneGate(step: AsyncJobStep | undefined): string | undefined {
333
+ const review = step?.review?.status ?? step?.acceptance?.reviewResult?.status;
334
+ if (review === "blockers") return "review blockers";
335
+ if (review === "review-required") return "review required";
336
+ if (review === "reviewed") return "reviewed";
337
+ const acceptance = step?.acceptance?.status;
338
+ if (acceptance === "review-required") return "acceptance review";
339
+ if (acceptance === "checked" || acceptance === "verified" || acceptance === "accepted") return "acceptance";
340
+ return undefined;
341
+ }
342
+
343
+ function laneNextAction(state: AsyncLaneProjection["state"], step: AsyncJobStep | undefined, output: string | undefined, gate: string | undefined): string | undefined {
344
+ if (step?.watchdog?.phase === "stale") return "inspect stale state";
345
+ if (step?.toolBudgetBlocked === true || step?.turnBudgetExceeded === true) return "inspect blocked state";
346
+ if (gate === "review blockers") return "resolve review blockers";
347
+ if (gate === "review required" || gate === "acceptance review") return "review output";
348
+ if (step?.activityState === "needs_attention") return "inspect attention";
349
+ if (state === "queued" || state === "pending") return "await launch";
350
+ if (state === "failed" || state === "rejected") return "inspect failure";
351
+ if (state === "partial") return "inspect partial state";
352
+ if (state === "paused") return "inspect paused state";
353
+ if (state === "stopped") return "inspect stopped state";
354
+ if ((state === "complete" || state === "completed") && gate === "reviewed") return "ready";
355
+ if ((state === "complete" || state === "completed") && output) return "inspect output";
356
+ return undefined;
357
+ }
358
+
359
+ function isTerminalLaneState(state: AsyncJobState["status"]): boolean {
360
+ return state !== "queued" && state !== "running";
361
+ }
362
+
363
+ /** Project already-loaded async status facts into one bounded, render-only lane row. */
364
+ export function projectAsyncLane(job: AsyncJobState, selectedStep = laneStepForJob(job)): AsyncLaneProjection | undefined {
365
+ const trace = laneTraceForJob(job);
366
+ const workspace = job.cwd ? boundedLaneValue(shortenPath(job.cwd)) : undefined;
367
+ const label = compactTaskText(selectedStep?.description, selectedStep?.label)
368
+ ?? boundedLaneValue(trace?.label)
369
+ ?? (workspace ? undefined : boundedLaneValue(selectedStep?.workflowKey ?? job.workflowKey));
370
+ const role = boundedLaneValue(selectedStep?.agent ?? trace?.agent ?? job.agents?.[0] ?? widgetJobName(job), 32) ?? "subagent";
371
+ const phase = boundedLaneValue(selectedStep?.phase ?? trace?.phase);
372
+ const gate = laneGate(selectedStep);
373
+ const output = boundedLaneValue(selectedStep?.outputName);
374
+ const ref = boundedLaneValue(selectedStep?.workflowKey ?? job.workflowKey ?? job.asyncId.slice(0, 8), 24) ?? job.asyncId.slice(0, 8);
375
+ const chips = [
376
+ selectedStep?.context,
377
+ selectedStep?.structured ? "structured" : undefined,
378
+ selectedStep?.activityState === "active_long_running" ? "long-running" : undefined,
379
+ selectedStep?.activityState === "needs_attention" ? "attention" : undefined,
380
+ selectedStep?.watchdog?.phase === "stale" ? "stale" : undefined,
381
+ selectedStep?.toolBudgetBlocked === true || selectedStep?.turnBudgetExceeded === true ? "blocked" : undefined,
382
+ ].filter((chip): chip is string => Boolean(chip));
383
+ const state = isTerminalLaneState(job.status) ? job.status : selectedStep?.status ?? job.status;
384
+ const next = laneNextAction(state, selectedStep, output, gate);
385
+ if (!label && !phase && !gate && !output && !selectedStep?.workflowKey && !job.workflowKey && !trace?.label && !trace?.phase) return undefined;
386
+ return { ...(label ? { label } : {}), role, ...(phase ? { phase } : {}), state, ...(gate ? { gate } : {}), ...(next ? { next } : {}), ...(output ? { output } : {}), ...(workspace ? { workspace } : {}), ref, chips };
387
+ }
388
+
389
+ function laneStateLabel(state: AsyncLaneProjection["state"], theme: Theme): string {
390
+ if (state === "running") return theme.fg("accent", "running");
391
+ if (state === "queued" || state === "pending") return theme.fg("muted", state);
392
+ if (state === "complete" || state === "completed") return theme.fg("success", "complete");
393
+ if (state === "failed" || state === "rejected") return theme.fg("error", state === "rejected" ? "rejected" : "failed");
394
+ return theme.fg("warning", state);
395
+ }
396
+
397
+ function formatLaneProjection(lane: AsyncLaneProjection, theme: Theme): string {
398
+ const label = boundedLaneValue(lane.label, 56);
399
+ const identity = [label, lane.role ? `role:${lane.role}` : undefined].filter(Boolean).join(" · ");
400
+ return `${identity ? theme.bold(identity) : theme.bold(lane.role)} · ${laneStateLabel(lane.state, theme)}`;
401
+ }
402
+
403
+ function formatLaneChip(chip: string, theme: Theme): string {
404
+ const text = `[${chip}]`;
405
+ if (chip === "blocked") return theme.fg("error", text);
406
+ if (chip === "stale") return theme.fg("warning", text);
407
+ return text;
408
+ }
409
+
410
+ function formatLaneProjectionDetails(lane: AsyncLaneProjection, theme: Theme): string | undefined {
411
+ const details = [
412
+ lane.phase ? `phase:${lane.phase}` : undefined,
413
+ lane.gate ? `gate:${lane.gate}` : undefined,
414
+ lane.next ? `next:${lane.next}` : undefined,
415
+ lane.output ? `out:${lane.output}` : undefined,
416
+ lane.workspace ? `workspace:${lane.workspace}` : lane.ref ? `ref:${lane.ref}` : undefined,
417
+ ].filter(Boolean);
418
+ const chips = lane.chips.map((chip) => formatLaneChip(chip, theme));
419
+ return [...(details.length ? [theme.fg("dim", details.join(" · "))] : []), ...chips].join(" · ") || undefined;
420
+ }
421
+
422
+ function formatLaneProjectionLines(lane: AsyncLaneProjection, theme: Theme, indent: string): string[] {
423
+ const details = formatLaneProjectionDetails(lane, theme);
424
+ return [
425
+ `${indent}${formatLaneProjection(lane, theme)}`,
426
+ ...(details ? [`${indent} ${details}`] : []),
427
+ ];
428
+ }
429
+
430
+ function laneRenderKey(job: AsyncJobState): unknown {
431
+ const lane = projectAsyncLane(job);
432
+ return lane ? [lane.label, lane.role, lane.phase, lane.state, lane.gate, lane.next, lane.output, lane.workspace, lane.ref, lane.chips] : undefined;
433
+ }
434
+
435
+ function widgetLaneDetailLines(job: AsyncJobState, theme: Theme): string[] {
436
+ if (job.steps?.length && (job.mode === "parallel" || job.mode === "chain")) return [];
437
+ const lane = projectAsyncLane(job);
438
+ return lane ? formatLaneProjectionLines(lane, theme, " ") : [];
439
+ }
440
+
441
+ function workflowPreflightLines(job: AsyncJobState, expanded = false): string[] {
442
+ if (job.mode !== "workflow" || !job.preflight) return [];
443
+ if (!expanded) {
444
+ const warning = formatWorkflowPreflightWarningSummary(job.workflow?.preflightWarnings, { indent: " ", hint: "expand for debug" });
445
+ return [
446
+ formatWorkflowPreflightPlanSummary(job.preflight, { indent: " " }),
447
+ ...(warning ? [warning] : []),
448
+ ];
449
+ }
450
+ return [
451
+ ...formatWorkflowPreflight(job.preflight, { indent: " " }).split("\n"),
452
+ ...(job.workflow?.preflightWarnings ? formatWorkflowPreflightWarnings(job.workflow.preflightWarnings, { indent: " " }).split("\n") : []),
453
+ ];
454
+ }
455
+
277
456
  function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
278
457
  const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
279
458
  const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
@@ -470,7 +649,7 @@ function resultStatusLine(result: Details["results"][number], output: string): s
470
649
 
471
650
  type ResultPresentation = {
472
651
  glyph: string;
473
- label: "running" | "detached" | "stopped" | "paused" | "failed" | "completed";
652
+ label: "running" | "detached" | "stopped" | "paused" | "failed" | "partial" | "completed";
474
653
  tone: "accent" | "warning" | "error" | "success";
475
654
  };
476
655
 
@@ -480,6 +659,7 @@ function semanticResultPresentation(input: {
480
659
  stopped?: boolean;
481
660
  interrupted?: boolean;
482
661
  failed?: boolean;
662
+ partial?: boolean;
483
663
  completedWithoutOutput?: boolean;
484
664
  seed?: number;
485
665
  frame?: number;
@@ -492,6 +672,7 @@ function semanticResultPresentation(input: {
492
672
  if (input.stopped) return { glyph: "■", label: "stopped", tone: "warning" };
493
673
  if (input.interrupted) return { glyph: "■", label: "paused", tone: "warning" };
494
674
  if (input.failed) return { glyph: "✗", label: "failed", tone: "error" };
675
+ if (input.partial) return { glyph: "■", label: "partial", tone: "warning" };
495
676
  return { glyph: "✓", label: "completed", tone: input.completedWithoutOutput ? "warning" : "success" };
496
677
  }
497
678
 
@@ -570,6 +751,7 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
570
751
  return [
571
752
  step.index ?? index,
572
753
  step.agent,
754
+ step.sessionName,
573
755
  step.workflowKey,
574
756
  step.phase,
575
757
  step.label,
@@ -589,6 +771,15 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
589
771
  step.model,
590
772
  step.thinking,
591
773
  step.context,
774
+ step.description,
775
+ step.outputName,
776
+ step.structured,
777
+ step.acceptance?.status,
778
+ step.acceptance?.reviewResult?.status,
779
+ step.review?.status,
780
+ step.toolBudgetBlocked,
781
+ step.turnBudgetExceeded,
782
+ step.watchdog?.phase,
592
783
  step.error,
593
784
  expanded ? expandedStepActivityRenderKey(step) : undefined,
594
785
  nestedRenderKey(step.children, expanded),
@@ -600,6 +791,7 @@ function nestedRenderKey(children: NestedRunSummary[] | undefined, expanded = fa
600
791
  child.id,
601
792
  child.state,
602
793
  child.agent,
794
+ child.sessionName,
603
795
  child.model,
604
796
  child.thinking,
605
797
  child.activityState,
@@ -619,6 +811,22 @@ function nestedRenderKey(children: NestedRunSummary[] | undefined, expanded = fa
619
811
  ]);
620
812
  }
621
813
 
814
+ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
815
+ return [
816
+ row.kind,
817
+ row.name,
818
+ row.state,
819
+ row.provider,
820
+ row.role,
821
+ row.verdict,
822
+ row.reasonCode,
823
+ row.detail,
824
+ row.target,
825
+ row.freshness,
826
+ row.reportPath,
827
+ ];
828
+ }
829
+
622
830
  export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
623
831
  return JSON.stringify({
624
832
  asyncDir: job.asyncDir,
@@ -635,8 +843,12 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
635
843
  currentStep: job.currentStep,
636
844
  chainStepCount: job.chainStepCount,
637
845
  parallelGroups: job.parallelGroups,
846
+ workflowHostSteps: projectAsyncWorkflowRows([], job.hostSteps).map(hostStepRenderKey),
847
+ preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
848
+ preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
638
849
  steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
639
850
  nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
851
+ lane: laneRenderKey(job),
640
852
  stepsTotal: job.stepsTotal,
641
853
  runningSteps: job.runningSteps,
642
854
  completedSteps: job.completedSteps,
@@ -677,6 +889,7 @@ function widgetActivity(job: AsyncJobState): string {
677
889
  if (job.status === "queued") return "queued…";
678
890
  if (job.status === "paused") return "Paused";
679
891
  if (job.status === "stopped") return "Stopped";
892
+ if (job.status === "partial") return "Partial";
680
893
  if (job.status === "failed") return "Failed";
681
894
  return "Done";
682
895
  }
@@ -794,9 +1007,11 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
794
1007
  const activity = widgetStepActivity(step, job.updatedAt);
795
1008
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
796
1009
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
797
- const label = compactTaskText(undefined, step.label);
798
- const display = label ? `${label} (${step.agent})` : step.agent;
1010
+ const label = compactTaskText(step.description, step.label);
1011
+ const display = step.sessionName?.trim() || (label ? `${label} (${step.agent})` : step.agent);
799
1012
  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}` : ""}`)}`);
1013
+ const lane = projectAsyncLane(job, step);
1014
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
800
1015
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
801
1016
  }
802
1017
  return lines;
@@ -922,12 +1137,13 @@ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): C
922
1137
  continue;
923
1138
  }
924
1139
  for (let index = span.start; index < span.start + span.count; index++) {
1140
+ const result = details.results[index];
925
1141
  entries.push({
926
1142
  kind: "result",
927
1143
  resultIndex: index,
928
1144
  rowNumber: index + 1,
929
1145
  rowLabel: span.isParallel ? `Agent ${index - span.start + 1}/${span.count}` : `Step ${span.stepIndex + 1}`,
930
- agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
1146
+ agentName: result?.sessionName?.trim() || result?.agent || details.chainAgents?.[span.stepIndex] || `step-${span.stepIndex + 1}`,
931
1147
  });
932
1148
  }
933
1149
  }
@@ -1083,7 +1299,11 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
1083
1299
  return statJoin(theme, [
1084
1300
  step.turnCount !== undefined ? `${step.turnCount} turns` : "",
1085
1301
  step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
1086
- step.tokens?.total ? formatTokenUsage(step.tokens, "token") : "",
1302
+ step.tokens
1303
+ ? step.contextLimit !== undefined
1304
+ ? formatContextUsage(step.tokens, step.contextLimit) ?? formatTokenUsage(step.tokens, "token")
1305
+ : step.tokens.total ? formatTokenUsage(step.tokens, "token") : ""
1306
+ : "",
1087
1307
  step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
1088
1308
  ]);
1089
1309
  }
@@ -1108,6 +1328,7 @@ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["s
1108
1328
  }
1109
1329
 
1110
1330
  function nestedRunName(run: NestedRunSummary): string {
1331
+ if (run.sessionName?.trim()) return run.sessionName.trim();
1111
1332
  if (run.agent) return run.agent;
1112
1333
  if (run.agents?.length) return formatWidgetAgents(run.agents);
1113
1334
  return run.id;
@@ -1117,6 +1338,7 @@ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary[
1117
1338
  if (state === "running") return theme.fg("accent", runningGlyph(seed));
1118
1339
  if (state === "complete" || state === "completed") return theme.fg("success", "✓");
1119
1340
  if (state === "failed") return theme.fg("error", "✗");
1341
+ if (state === "partial") return theme.fg("warning", "■");
1120
1342
  if (state === "paused") return theme.fg("warning", "■");
1121
1343
  if (state === "stopped") return theme.fg("warning", "■");
1122
1344
  return theme.fg("muted", "◦");
@@ -1164,6 +1386,7 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
1164
1386
  if (state === "queued" || state === "pending") return "queued…";
1165
1387
  if (state === "paused") return "Paused";
1166
1388
  if (state === "stopped") return "Stopped";
1389
+ if (state === "partial") return "Partial";
1167
1390
  if (state === "failed") return "Failed";
1168
1391
  return "Done";
1169
1392
  }
@@ -1188,7 +1411,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
1188
1411
  const activity = nestedActivity(step, state, snapshotNow ?? fallback);
1189
1412
  const timestamp = "status" in step ? nestedStepTimestamp(step, fallback) : formatClockTime(nestedRunEventTime(step));
1190
1413
  const error = step.error ? ` · ${step.error}` : "";
1191
- const name = "status" in step ? step.agent : nestedRunName(step);
1414
+ const name = "status" in step ? childDisplayName(step) : nestedRunName(step);
1192
1415
  rows.push({
1193
1416
  prefix,
1194
1417
  text: `${nestedTimestampPrefix(timestamp)}${nestedStatusGlyph(state, theme)} ${name} · ${state}${modelThinking ? ` · ${modelThinking}` : ""}${activity ? ` · ${activity}` : ""}${error}`,
@@ -1250,7 +1473,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
1250
1473
  for (const step of child.steps ?? []) {
1251
1474
  if (lines.length >= lineBudget) return;
1252
1475
  const modelThinking = formatModelThinking(step.model, step.thinking);
1253
- 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)}`));
1476
+ 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)}`));
1254
1477
  append(step.children, depth + 1, `${prefix} `);
1255
1478
  }
1256
1479
  append(child.children, depth + 1, `${prefix} `);
@@ -1274,7 +1497,9 @@ function foregroundStyleWidgetStepLines(
1274
1497
  const status = widgetStepStatus(step.status, theme);
1275
1498
  const stats = widgetStepStats(theme, step);
1276
1499
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1277
- 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}` : ""}`];
1500
+ const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, childDisplayName(step))}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1501
+ const lane = projectAsyncLane(job, step);
1502
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
1278
1503
  const task = compactTaskText(step.description, step.label);
1279
1504
  if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
1280
1505
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
@@ -1302,18 +1527,50 @@ function foregroundStyleWidgetStepLines(
1302
1527
  return lines;
1303
1528
  }
1304
1529
 
1530
+ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string): string[] {
1531
+ const rows = projectAsyncWorkflowRows([], job.hostSteps).filter((row) => row.kind);
1532
+ const visible = rows.slice(0, 8);
1533
+ const lines = visible.map((row) => {
1534
+ const state = hostStepVerdictLabel(row.state as HostStepState, row.verdict as HostStepVerdict | undefined);
1535
+ const glyph = state === "running" ? theme.fg("accent", "●")
1536
+ : state === "pending" ? theme.fg("muted", "◦")
1537
+ : state === "pass" ? theme.fg("success", "✓")
1538
+ : state === "fail" || state === "error" ? theme.fg("error", "✗")
1539
+ : theme.fg("warning", "■");
1540
+ const details = [
1541
+ row.provider ? `provider:${row.provider}` : undefined,
1542
+ row.role ? `role:${row.role}` : undefined,
1543
+ row.target,
1544
+ row.detail,
1545
+ row.reasonCode ? `reason:${row.reasonCode}` : undefined,
1546
+ row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : undefined,
1547
+ row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : undefined,
1548
+ ].filter(Boolean).join(" · ");
1549
+ return `${indent}${glyph} ${row.kind}: ${row.name} · ${state}${details ? ` · ${details}` : ""}`;
1550
+ });
1551
+ if (rows.length > visible.length) lines.push(`${indent}${theme.fg("dim", `… +${rows.length - visible.length} host steps hidden`)}`);
1552
+ return lines;
1553
+ }
1554
+
1305
1555
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
1306
- if (!job.steps?.length) return [
1307
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1308
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
1309
- ];
1556
+ if (!job.steps?.length) {
1557
+ const lane = projectAsyncLane(job);
1558
+ return [
1559
+ ...workflowPreflightLines(job, expanded),
1560
+ ...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
1561
+ ...hostStepWidgetLines(job, theme, " "),
1562
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1563
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
1564
+ ];
1565
+ }
1310
1566
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
1311
1567
  const total = job.stepsTotal ?? job.steps.length;
1312
1568
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
1313
- const lines: string[] = [];
1569
+ const lines: string[] = workflowPreflightLines(job, expanded);
1314
1570
  for (const [index, step] of job.steps.entries()) {
1315
1571
  lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, frame));
1316
1572
  }
1573
+ lines.push(...hostStepWidgetLines(job, theme, " "));
1317
1574
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
1318
1575
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
1319
1576
  for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
@@ -1349,9 +1606,12 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1349
1606
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1350
1607
  const task = compactTaskText(step.description, step.label);
1351
1608
  const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
1352
- 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}` : ""}`);
1609
+ lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, childDisplayName(step))}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1610
+ const lane = projectAsyncLane(job, step);
1611
+ if (lane) lines.push(...formatLaneProjectionLines(lane, theme, " "));
1353
1612
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
1354
1613
  }
1614
+ lines.push(...hostStepWidgetLines(job, theme, " "));
1355
1615
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
1356
1616
  return lines.map((line) => truncLine(line, width));
1357
1617
  }
@@ -1488,9 +1748,18 @@ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, fra
1488
1748
  const stats = widgetStats(job, theme);
1489
1749
  const activity = widgetActivity(job);
1490
1750
  const status = job.status === "complete" ? "done" : job.status;
1751
+ const lane = projectAsyncLane(job);
1752
+ const laneSummary = lane
1753
+ ? [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(" · ")
1754
+ : "";
1755
+ const laneSignals = lane
1756
+ ? [lane.next ? `next:${lane.next}` : undefined, ...lane.chips.map((chip) => formatLaneChip(chip, theme))].filter(Boolean).join(" · ")
1757
+ : "";
1491
1758
  const parts = [
1492
1759
  `${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}`,
1493
1760
  theme.fg("dim", status),
1761
+ laneSignals ? laneSignals : "",
1762
+ laneSummary ? theme.fg("dim", laneSummary) : "",
1494
1763
  stats,
1495
1764
  activity && activity.toLowerCase() !== status ? theme.fg("dim", activity) : "",
1496
1765
  ].filter(Boolean);
@@ -1636,6 +1905,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1636
1905
  items.push([
1637
1906
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1638
1907
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1908
+ ...widgetLaneDetailLines(job, theme),
1639
1909
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1640
1910
  ]);
1641
1911
  slots--;
@@ -1653,6 +1923,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1653
1923
  items.push([
1654
1924
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1655
1925
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1926
+ ...widgetLaneDetailLines(job, theme),
1656
1927
  ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1657
1928
  ]);
1658
1929
  slots--;
@@ -1720,7 +1991,7 @@ function renderSingleCompact(
1720
1991
  const detailIndent = mainWindowIndent(layout, 1);
1721
1992
  const continuationIndent = mainWindowIndent(layout, 2) + (layout.horizontalSpacing > 0 ? " " : "");
1722
1993
  const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1723
- 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));
1994
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(childDisplayName(r)))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1724
1995
 
1725
1996
  if (isRunning && r.progress) {
1726
1997
  const task = compactTaskText(r.task);
@@ -1753,6 +2024,7 @@ function renderSingleCompact(
1753
2024
  }
1754
2025
 
1755
2026
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
2027
+ if (row.state === "planned") return theme.fg("muted", "◦");
1756
2028
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1757
2029
  if (row.state === "complete") return theme.fg("success", "✓");
1758
2030
  if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", "■");
@@ -1761,6 +2033,7 @@ function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: nu
1761
2033
 
1762
2034
  function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): string {
1763
2035
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
2036
+ if (row.state === "planned") return theme.fg("dim", label);
1764
2037
  if (row.state === "running") return theme.fg("accent", label);
1765
2038
  if (row.state === "complete") return theme.fg("success", label);
1766
2039
  if (row.state === "detached" || row.state === "stopped") return theme.fg("warning", label);
@@ -1775,9 +2048,9 @@ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue:
1775
2048
  return "running";
1776
2049
  }
1777
2050
 
1778
- function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
2051
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number, expanded = false): Component {
1779
2052
  const workflow = d.workflow;
1780
- const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
2053
+ const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace, d.preflight) : d.preflight ? buildWorkflowChatProgressRows([], d.preflight) : [];
1781
2054
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1782
2055
  const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : state === "paused" ? theme.fg("warning", "■") : theme.fg("error", "✗");
1783
2056
  const width = getTermWidth() - 4;
@@ -1788,6 +2061,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1788
2061
  const rowIndent = mainWindowIndent(layout, 1);
1789
2062
  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));
1790
2063
  c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Repo ${repoLabel}`), width), 0, 0));
2064
+ if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
1791
2065
  if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
1792
2066
  if (rows.length === 0) {
1793
2067
  c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ waiting for workflow child launches`), width), 0, 0));
@@ -1801,7 +2075,20 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1801
2075
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1802
2076
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1803
2077
  const error = row.error ? ` ${theme.fg(row.state === "detached" ? "warning" : "error", `· ${compactWorkflowError(row.error)}`)}` : "";
1804
- c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
2078
+ const hints = expanded && row.preflight ? [
2079
+ row.preflight.mode ? `mode:${row.preflight.mode}` : undefined,
2080
+ row.preflight.decision ? `decision:${row.preflight.decision}` : undefined,
2081
+ row.preflight.claims?.length ? `claims:${row.preflight.claims.join(",")}` : undefined,
2082
+ row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
2083
+ row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
2084
+ ].filter((value): value is string => Boolean(value)).join(" · ") : "";
2085
+ 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));
2086
+ }
2087
+ if (workflow?.preflightWarnings?.length) {
2088
+ const warningLines = expanded
2089
+ ? formatWorkflowPreflightWarnings(workflow.preflightWarnings, { indent: rowIndent }).split("\n")
2090
+ : [formatWorkflowPreflightWarningSummary(workflow.preflightWarnings, { indent: rowIndent, hint: "expand for debug" })];
2091
+ for (const warningLine of warningLines) c.addChild(new Text(truncLine(theme.fg("warning", warningLine), width), 0, 0));
1805
2092
  }
1806
2093
  if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
1807
2094
  return c;
@@ -1817,6 +2104,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1817
2104
  || workflowGraphHasStatus(d, ["failed"]);
1818
2105
  const paused = d.results.some((r) => r.interrupted)
1819
2106
  || workflowGraphHasStatus(d, ["paused"]);
2107
+ const partial = workflowGraphHasStatus(d, ["partial"]);
1820
2108
  let totalSummary = d.progressSummary;
1821
2109
  if (!totalSummary) {
1822
2110
  let sawProgress = false;
@@ -1840,6 +2128,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1840
2128
  stopped,
1841
2129
  interrupted: paused,
1842
2130
  failed,
2131
+ partial,
1843
2132
  seed: runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex),
1844
2133
  frame,
1845
2134
  });
@@ -1849,7 +2138,6 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1849
2138
  const width = getTermWidth() - 4;
1850
2139
  const rowIndent = mainWindowIndent(layout, 1);
1851
2140
  const detailIndent = mainWindowIndent(layout, 2);
1852
- const continuationIndent = mainWindowIndent(layout, 3) + (layout.horizontalSpacing > 0 ? " " : "");
1853
2141
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1854
2142
 
1855
2143
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
@@ -1861,7 +2149,8 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1861
2149
  const r = d.results[i];
1862
2150
  const fallbackLabel = itemTitle.toLowerCase();
1863
2151
  const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1864
- return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
2152
+ const fallbackAgent = useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`);
2153
+ return { kind: "result", resultIndex: i, rowNumber, agentName: r?.sessionName?.trim() || fallbackAgent };
1865
2154
  });
1866
2155
  for (const entry of renderEntries) {
1867
2156
  if (entry.kind === "placeholder") {
@@ -1943,7 +2232,8 @@ export function renderSubagentSummary(
1943
2232
  const failed = result.isError === true
1944
2233
  || results.some((entry) => !hasTerminalResultFlag(entry) && entry.exitCode !== 0 && !isResultRunning(entry))
1945
2234
  || Boolean(details && workflowGraphHasStatus(details, ["failed"]));
1946
- const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : "completed";
2235
+ const partial = Boolean(details && workflowGraphHasStatus(details, ["partial"]));
2236
+ const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : partial ? "partial" : "completed";
1947
2237
  const glyph = state === "running"
1948
2238
  ? theme.fg("accent", STATIC_RUNNING_GLYPH)
1949
2239
  : state === "completed"
@@ -1952,7 +2242,7 @@ export function renderSubagentSummary(
1952
2242
  ? theme.fg("error", "✗")
1953
2243
  : theme.fg("warning", "■");
1954
2244
  const label = details?.mode === "single" && results.length === 1
1955
- ? results[0]?.agent || "subagent"
2245
+ ? childDisplayName(results[0])
1956
2246
  : details?.mode || "subagent";
1957
2247
  return new Text(
1958
2248
  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),
@@ -1976,7 +2266,7 @@ export function renderSubagentResult(
1976
2266
  const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
1977
2267
  const d = result.details;
1978
2268
  if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && d.workflow?.value === undefined && (!result.isError || (d.workflow?.trace.length ?? 0) > 0)) {
1979
- return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
2269
+ return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame, options.expanded));
1980
2270
  }
1981
2271
  if (!d || !d.results.length) {
1982
2272
  const t = result.content[0];
@@ -1987,9 +2277,12 @@ export function renderSubagentResult(
1987
2277
  if (d && !options.expanded && !result.isError) {
1988
2278
  const lines = text.split(/\r?\n/);
1989
2279
  const firstNonEmptyLine = lines.find((line) => line.trim())?.trim() || "(no output)";
2280
+ const compactLine = d.mode === "workflow" && d.preflight
2281
+ ? formatWorkflowPreflightPlanSummary(d.preflight)
2282
+ : firstNonEmptyLine;
1990
2283
  const c = new Container();
1991
2284
  const detailIndent = mainWindowIndent(layout, 1);
1992
- c.addChild(new Text(truncLine(`${contextPrefix}${firstNonEmptyLine} · ${lines.length} lines`, width), 0, 0));
2285
+ c.addChild(new Text(truncLine(`${contextPrefix}${compactLine} · ${lines.length} lines`, width), 0, 0));
1993
2286
  c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}Press ${liveDetailKeyText()} for full output`), width), 0, 0));
1994
2287
  return compact(c);
1995
2288
  }
@@ -2022,7 +2315,7 @@ export function renderSubagentResult(
2022
2315
  const fit = (text: string) => expanded ? text : truncLine(text, w);
2023
2316
  const toolCallLines = getToolCallLines(r, expanded);
2024
2317
  const c = new Container();
2025
- c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
2318
+ c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(childDisplayName(r)))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
2026
2319
  c.addChild(new Spacer(1));
2027
2320
  const taskMaxLen = Math.max(20, w - 8);
2028
2321
  const taskPreview = expanded || r.task.length <= taskMaxLen
@@ -2110,6 +2403,7 @@ export function renderSubagentResult(
2110
2403
  || workflowGraphHasStatus(d, ["failed"]);
2111
2404
  const paused = d.results.some((r) => r.interrupted)
2112
2405
  || workflowGraphHasStatus(d, ["paused"]);
2406
+ const partial = workflowGraphHasStatus(d, ["partial"]);
2113
2407
  const completedWithoutOutput = d.results.some((r) =>
2114
2408
  !hasTerminalResultFlag(r)
2115
2409
  && r.exitCode === 0
@@ -2122,6 +2416,7 @@ export function renderSubagentResult(
2122
2416
  stopped,
2123
2417
  interrupted: paused,
2124
2418
  failed,
2419
+ partial,
2125
2420
  completedWithoutOutput,
2126
2421
  frame,
2127
2422
  }), theme);
@@ -2161,6 +2456,7 @@ export function renderSubagentResult(
2161
2456
  ? d.chainAgents
2162
2457
  .map((agent, i) => {
2163
2458
  const result = d.results[i];
2459
+ const displayName = result?.sessionName?.trim() || agent;
2164
2460
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
2165
2461
  const stepPresentation = result
2166
2462
  ? styledResultPresentation(resultPresentation(result, getSingleResultOutput(result), isCurrent && hasRunning && !hasTerminalResultFlag(result)), theme)
@@ -2168,7 +2464,7 @@ export function renderSubagentResult(
2168
2464
  const stepStatus = stepPresentation
2169
2465
  ? `${stepPresentation.glyph} ${stepPresentation.label}`
2170
2466
  : theme.fg("dim", "◦ pending");
2171
- return `${stepStatus} ${agent}${contextModeBadge(theme, result?.context)}`;
2467
+ return `${stepStatus} ${displayName}${contextModeBadge(theme, result?.context)}`;
2172
2468
  })
2173
2469
  .join(theme.fg("dim", " → "))
2174
2470
  : null;
@@ -2195,7 +2491,8 @@ export function renderSubagentResult(
2195
2491
  const i = displayStart + offset;
2196
2492
  const r = d.results[i];
2197
2493
  const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
2198
- return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
2494
+ const fallbackAgent = useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
2495
+ return { kind: "result", resultIndex: i, rowNumber, agentName: r?.sessionName?.trim() || fallbackAgent };
2199
2496
  });
2200
2497
 
2201
2498
  c.addChild(new Spacer(1));
@@ -2235,8 +2532,8 @@ export function renderSubagentResult(
2235
2532
  const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
2236
2533
  const contextBadge = contextModeBadge(theme, r.context);
2237
2534
  const stepHeader = rRunning
2238
- ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
2239
- : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(r.agent)}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
2535
+ ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", childDisplayName(r)))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
2536
+ : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(childDisplayName(r))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
2240
2537
  const toolCallLines = getToolCallLines(r, expanded);
2241
2538
  c.addChild(new Text(fit(stepHeader), 0, 0));
2242
2539