pi-subagents 0.40.0 → 0.42.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 (121) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +54 -1879
  3. package/agents/oracle.md +1 -0
  4. package/inspector-runner.mjs +11 -0
  5. package/package.json +13 -5
  6. package/prompts/parallel-context-build.md +1 -1
  7. package/prompts/parallel-handoff-plan.md +1 -1
  8. package/prompts/review-loop.md +1 -1
  9. package/skills/pi-subagents/SKILL.md +9 -7
  10. package/skills/pi-subagents/references/constraints-and-recipes.md +24 -32
  11. package/skills/pi-subagents/references/execution-controls.md +99 -97
  12. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  13. package/skills/pi-subagents/references/prompting-and-roles.md +23 -30
  14. package/src/agents/agent-management.ts +155 -65
  15. package/src/agents/agent-serializer.ts +19 -0
  16. package/src/agents/agents.ts +154 -71
  17. package/src/agents/chain-serializer.ts +10 -7
  18. package/src/agents/frontmatter.ts +5 -3
  19. package/src/agents/identity.ts +1 -1
  20. package/src/agents/proactive-skills.ts +13 -10
  21. package/src/agents/skills.ts +23 -6
  22. package/src/api/control-channel.ts +4 -0
  23. package/src/api/delegation.ts +26 -194
  24. package/src/api/external-runs.ts +129 -0
  25. package/src/api/intercom-bridge.ts +3 -0
  26. package/src/api/pi-args.ts +5 -0
  27. package/src/api/preflight.ts +3 -3
  28. package/src/api/shared-types.ts +19 -0
  29. package/src/extension/config.ts +10 -0
  30. package/src/extension/control-notices.ts +5 -39
  31. package/src/extension/doctor.ts +10 -9
  32. package/src/extension/fanout-child.ts +7 -4
  33. package/src/extension/index.ts +232 -68
  34. package/src/extension/rpc.ts +18 -12
  35. package/src/extension/schemas.ts +48 -37
  36. package/src/extension/tool-description.ts +36 -86
  37. package/src/inspectors/herdr/actions.ts +229 -0
  38. package/src/inspectors/herdr/client.ts +130 -0
  39. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  40. package/src/inspectors/herdr/project-panes.ts +154 -0
  41. package/src/integrations/herdr-status.ts +330 -0
  42. package/src/intercom/intercom-bridge.ts +3 -2
  43. package/src/intercom/result-intercom.ts +5 -1
  44. package/src/missions/actions.ts +374 -0
  45. package/src/missions/lifecycle.ts +314 -0
  46. package/src/missions/store.ts +442 -0
  47. package/src/missions/types.ts +135 -0
  48. package/src/policy/authority.ts +46 -0
  49. package/src/profiles/profiles.ts +29 -3
  50. package/src/runs/background/async-execution.ts +110 -50
  51. package/src/runs/background/async-job-tracker.ts +27 -4
  52. package/src/runs/background/async-resume.ts +6 -6
  53. package/src/runs/background/async-status.ts +29 -1
  54. package/src/runs/background/auto-drain.ts +3 -3
  55. package/src/runs/background/chain-append.ts +3 -2
  56. package/src/runs/background/control-channel.ts +9 -7
  57. package/src/runs/background/fleet-view.ts +3 -4
  58. package/src/runs/background/notify.ts +2 -1
  59. package/src/runs/background/process-terminal.ts +5 -5
  60. package/src/runs/background/result-watcher.ts +13 -5
  61. package/src/runs/background/run-id-resolver.ts +3 -3
  62. package/src/runs/background/run-status.ts +35 -8
  63. package/src/runs/background/scheduled-runs.ts +602 -375
  64. package/src/runs/background/stale-run-reconciler.ts +3 -3
  65. package/src/runs/background/subagent-runner.ts +608 -445
  66. package/src/runs/background/subagent-wait.ts +50 -9
  67. package/src/runs/background/wait-subscriptions.ts +253 -0
  68. package/src/runs/background/wait-tool.ts +12 -4
  69. package/src/runs/foreground/async-steering-action.ts +3 -3
  70. package/src/runs/foreground/chain-clarify.ts +8 -4
  71. package/src/runs/foreground/chain-execution.ts +56 -30
  72. package/src/runs/foreground/execution.ts +34 -9
  73. package/src/runs/foreground/subagent-executor.ts +1036 -274
  74. package/src/runs/shared/acceptance.ts +28 -6
  75. package/src/runs/shared/child-protocol.ts +302 -22
  76. package/src/runs/shared/dynamic-fanout.ts +1 -1
  77. package/src/runs/shared/external-cli-runner.ts +130 -0
  78. package/src/runs/shared/long-running-guard.ts +42 -1
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
  80. package/src/runs/shared/nested-events.ts +59 -5
  81. package/src/runs/shared/nested-render.ts +9 -4
  82. package/src/runs/shared/parallel-handoff.ts +86 -2
  83. package/src/runs/shared/parallel-utils.ts +11 -2
  84. package/src/runs/shared/permissions.ts +95 -0
  85. package/src/runs/shared/pi-args.ts +468 -109
  86. package/src/runs/shared/pi-spawn.ts +11 -1
  87. package/src/runs/shared/run-history.ts +1 -1
  88. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  89. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  90. package/src/runs/shared/turn-budget.ts +6 -6
  91. package/src/runs/shared/worktree.ts +122 -12
  92. package/src/shared/accessible-dir.ts +29 -7
  93. package/src/shared/artifacts.ts +143 -1
  94. package/src/shared/fork-context.ts +3 -2
  95. package/src/shared/launch-contract.ts +1 -0
  96. package/src/shared/settings.ts +10 -0
  97. package/src/shared/types.ts +156 -14
  98. package/src/shared/utils.ts +8 -6
  99. package/src/slash/delegation-adapters.ts +32 -194
  100. package/src/slash/delegation-request.ts +43 -126
  101. package/src/slash/prompt-template-bridge.ts +158 -205
  102. package/src/slash/prompt-workflows.ts +21 -57
  103. package/src/slash/slash-bridge.ts +14 -0
  104. package/src/slash/slash-commands.ts +31 -632
  105. package/src/slash/subagents-admin.ts +18 -14
  106. package/src/tui/fleet-status.ts +156 -21
  107. package/src/tui/fleet-transcript.ts +110 -5
  108. package/src/tui/fleet.ts +57 -25
  109. package/src/tui/render.ts +368 -152
  110. package/src/types/pi-runtime-compat.d.ts +8 -0
  111. package/src/watchdog/lsp-diagnostics.ts +12 -7
  112. package/src/watchdog/model-selection.ts +2 -2
  113. package/src/watchdog/permission-arbiter.ts +145 -0
  114. package/src/watchdog/register-child.ts +1 -1
  115. package/src/watchdog/register-main.ts +1 -1
  116. package/src/watchdog/review.ts +4 -1
  117. package/src/watchdog/runtime.ts +3 -2
  118. package/src/workflows/chat-progress.ts +140 -0
  119. package/src/workflows/scripted-workflow.ts +415 -0
  120. package/agents/advisor.md +0 -73
  121. package/src/extension/chain-validation.ts +0 -181
package/src/tui/render.ts CHANGED
@@ -24,6 +24,7 @@ import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
24
24
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
25
25
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
26
26
  import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode.ts";
27
+ import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../workflows/chat-progress.ts";
27
28
 
28
29
  type Theme = ExtensionContext["ui"]["theme"];
29
30
 
@@ -143,6 +144,24 @@ function runningGlyph(seed?: number): string {
143
144
  return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
144
145
  }
145
146
 
147
+ const WIDGET_ANIMATION_FRAME_MS = 500;
148
+ let widgetRequestRender: (() => void) | undefined;
149
+
150
+ function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
151
+ if (frame === undefined) return seed;
152
+ return (seed ?? 0) + frame;
153
+ }
154
+
155
+ function widgetAnimationFrame(): number {
156
+ return Math.floor(Date.now() / WIDGET_ANIMATION_FRAME_MS);
157
+ }
158
+
159
+ export function requestWidgetRender(): boolean {
160
+ if (!widgetRequestRender) return false;
161
+ widgetRequestRender();
162
+ return true;
163
+ }
164
+
146
165
  function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
147
166
  if (!progress) return undefined;
148
167
  return runningSeed(
@@ -277,17 +296,81 @@ function resultStatusLine(result: Details["results"][number], output: string): s
277
296
  return "Done";
278
297
  }
279
298
 
280
- function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): string {
281
- if (running) {
282
- if (frame !== undefined) return theme.fg("accent", runningGlyph((seed ?? 0) + frame));
283
- return theme.fg("accent", runningGlyph(seed));
299
+ type ResultPresentation = {
300
+ glyph: string;
301
+ label: "running" | "detached" | "stopped" | "paused" | "failed" | "completed";
302
+ tone: "accent" | "warning" | "error" | "success";
303
+ };
304
+
305
+ function semanticResultPresentation(input: {
306
+ running?: boolean;
307
+ detached?: boolean;
308
+ stopped?: boolean;
309
+ interrupted?: boolean;
310
+ failed?: boolean;
311
+ completedWithoutOutput?: boolean;
312
+ seed?: number;
313
+ frame?: number;
314
+ }): ResultPresentation {
315
+ if (input.running) {
316
+ const glyph = input.frame !== undefined ? runningGlyph((input.seed ?? 0) + input.frame) : runningGlyph(input.seed);
317
+ return { glyph, label: "running", tone: "accent" };
284
318
  }
285
- if (result.detached) return theme.fg("warning", "■");
286
- if (result.stopped) return theme.fg("warning", "■");
287
- if (result.interrupted) return theme.fg("warning", "■");
288
- if (result.exitCode !== 0) return theme.fg("error", "✗");
289
- if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
290
- return theme.fg("success", "✓");
319
+ if (input.detached) return { glyph: "■", label: "detached", tone: "warning" };
320
+ if (input.stopped) return { glyph: "■", label: "stopped", tone: "warning" };
321
+ if (input.interrupted) return { glyph: "■", label: "paused", tone: "warning" };
322
+ if (input.failed) return { glyph: "✗", label: "failed", tone: "error" };
323
+ return { glyph: "✓", label: "completed", tone: input.completedWithoutOutput ? "warning" : "success" };
324
+ }
325
+
326
+ function hasTerminalResultFlag(result: Details["results"][number]): boolean {
327
+ return Boolean(result.detached || result.stopped || result.interrupted);
328
+ }
329
+
330
+ function hasTerminalResult(result: Details["results"][number]): boolean {
331
+ if (hasTerminalResultFlag(result)) return true;
332
+ const status = result.progress?.status;
333
+ if (status === "running" || status === "pending") return false;
334
+ return result.exitCode !== undefined;
335
+ }
336
+
337
+ function isResultRunning(result: Details["results"][number], status = result.progress?.status): boolean {
338
+ return status === "running" && !hasTerminalResultFlag(result);
339
+ }
340
+
341
+ function detailsHaveRunningResult(details: Details): boolean {
342
+ return details.progress?.some((progress) => {
343
+ if (progress.status !== "running") return false;
344
+ const result = details.results.find((entry) => entry.progress?.index === progress.index) ?? details.results[progress.index];
345
+ return !result || !hasTerminalResultFlag(result);
346
+ })
347
+ || details.results.some((result) => isResultRunning(result))
348
+ || workflowGraphHasStatus(details, ["running"]);
349
+ }
350
+
351
+ function resultPresentation(result: Details["results"][number], output: string, running = isResultRunning(result), seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): ResultPresentation {
352
+ return semanticResultPresentation({
353
+ running,
354
+ detached: result.detached,
355
+ stopped: result.stopped,
356
+ interrupted: result.interrupted,
357
+ failed: result.exitCode !== 0,
358
+ completedWithoutOutput: hasEmptyTextOutputWithoutOutputTarget(result.task, output),
359
+ seed,
360
+ frame,
361
+ });
362
+ }
363
+
364
+ function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = isResultRunning(result), seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): string {
365
+ const presentation = resultPresentation(result, output, running, seed, frame);
366
+ return theme.fg(presentation.tone, presentation.glyph);
367
+ }
368
+
369
+ function styledResultPresentation(presentation: ResultPresentation, theme: Theme): { glyph: string; label: string } {
370
+ return {
371
+ glyph: theme.fg(presentation.tone, presentation.glyph),
372
+ label: theme.fg(presentation.tone, presentation.label),
373
+ };
291
374
  }
292
375
 
293
376
  function compactCurrentActivity(progress: AgentProgress): string {
@@ -396,8 +479,8 @@ function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
396
479
  return seed;
397
480
  }
398
481
 
399
- function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
400
- if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job)));
482
+ function widgetStatusGlyph(job: AsyncJobState, theme: Theme, frame?: number): string {
483
+ if (job.status === "running") return theme.fg("accent", runningGlyph(animatedSeed(widgetJobRunningSeed(job), frame)));
401
484
  if (job.status === "queued") return theme.fg("muted", "◦");
402
485
  if (job.status === "complete") return theme.fg("success", "✓");
403
486
  if (job.status === "paused") return theme.fg("warning", "■");
@@ -405,8 +488,8 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
405
488
  return theme.fg("error", "✗");
406
489
  }
407
490
 
408
- function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
409
- if (status === "running") return theme.fg("accent", runningGlyph(seed));
491
+ function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number, frame?: number): string {
492
+ if (status === "running") return theme.fg("accent", runningGlyph(animatedSeed(seed, frame)));
410
493
  if (status === "complete" || status === "completed") return theme.fg("success", "✓");
411
494
  if (status === "failed") return theme.fg("error", "✗");
412
495
  if (status === "paused") return theme.fg("warning", "■");
@@ -471,7 +554,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
471
554
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
472
555
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
473
556
  lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
474
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
557
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
475
558
  }
476
559
  return lines;
477
560
  }
@@ -639,14 +722,12 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
639
722
  || details.progress?.find((progress) => progress.agent === result.agent && progress.status === "running");
640
723
  const index = result.progress?.index ?? progressFromArray?.index ?? i;
641
724
  if (index < 0 || index >= totalCount) continue;
642
- const status = result.progress?.status
643
- ?? (result.stopped
644
- ? "stopped"
645
- : result.interrupted || result.detached
646
- ? "detached"
647
- : result.exitCode === 0
648
- ? "completed"
649
- : "failed");
725
+ const status = result.stopped
726
+ ? "stopped"
727
+ : result.interrupted || result.detached
728
+ ? "detached"
729
+ : result.progress?.status
730
+ ?? (result.exitCode === 0 ? "completed" : "failed");
650
731
  statuses[index] = status;
651
732
  }
652
733
  const running = statuses.filter((status) => status === "running").length;
@@ -667,8 +748,8 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
667
748
  let done = 0;
668
749
  for (let index = groupStart; index < groupEnd; index++) {
669
750
  const progressEntry = details.progress?.find((progress) => progress.index === index);
670
- const resultEntry = details.results.find((result) => result.progress?.index === index);
671
- if (progressEntry?.status === "running") {
751
+ const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
752
+ if (progressEntry?.status === "running" && (!resultEntry || !hasTerminalResultFlag(resultEntry))) {
672
753
  running++;
673
754
  continue;
674
755
  }
@@ -847,9 +928,56 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
847
928
  function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
848
929
  if (!children?.length || lineBudget <= 0) return [];
849
930
  if (!expanded) {
850
- const aggregate = formatNestedAggregate(children);
851
- const latest = children.reduce((acc, child) => Math.max(acc, nestedRunEventTime(child) ?? 0), 0);
852
- return aggregate ? [theme.fg("dim", `↳ ${nestedTimestampPrefix(formatClockTime(latest))}${aggregate}`)] : [];
931
+ type CollapsedRow = { text: string; prefix: string };
932
+ const rows: CollapsedRow[] = [];
933
+ const maxLeaves = 4;
934
+ const maxLines = Math.min(6, lineBudget);
935
+ let leaves = 0;
936
+ let overflow = 0;
937
+ const appendLeaf = (step: NestedStepSummary | NestedRunSummary, prefix: string, fallback?: number): void => {
938
+ if (leaves >= maxLeaves) {
939
+ overflow++;
940
+ return;
941
+ }
942
+ leaves++;
943
+ const state = "status" in step ? step.status : step.state;
944
+ const modelThinking = formatModelThinking(step.model, step.thinking);
945
+ const activity = nestedActivity(step, state, snapshotNow ?? fallback);
946
+ const timestamp = "status" in step ? nestedStepTimestamp(step, fallback) : formatClockTime(nestedRunEventTime(step));
947
+ const error = step.error ? ` · ${step.error}` : "";
948
+ const name = "status" in step ? step.agent : nestedRunName(step);
949
+ rows.push({
950
+ prefix,
951
+ text: `${nestedTimestampPrefix(timestamp)}${nestedStatusGlyph(state, theme)} ${name} · ${state}${modelThinking ? ` · ${modelThinking}` : ""}${activity ? ` · ${activity}` : ""}${error}`,
952
+ });
953
+ };
954
+ for (const child of children) {
955
+ const steps = (child.mode === "parallel" || child.mode === "chain") ? child.steps ?? [] : [];
956
+ if (steps.length > 0) {
957
+ const ownerModelThinking = formatModelThinking(child.model, child.thinking);
958
+ const ownerActivity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
959
+ const ownerError = child.error ? ` · ${child.error}` : "";
960
+ rows.push({
961
+ prefix: "↳ ",
962
+ text: `OWNER ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state}${ownerModelThinking ? ` · ${ownerModelThinking}` : ""}${ownerActivity ? ` · ${ownerActivity}` : ""}${ownerError}`,
963
+ });
964
+ for (const step of steps) appendLeaf(step, "↳ │ ", child.lastUpdate);
965
+ } else {
966
+ appendLeaf(child, "↳ ", child.lastUpdate);
967
+ }
968
+ }
969
+ if (overflow > 0) rows.push({ prefix: "↳ ", text: `… +${overflow} more nested leaves` });
970
+ const visibleRows = rows.length <= maxLines
971
+ ? rows
972
+ : overflow > 0
973
+ ? [...rows.slice(0, Math.max(0, maxLines - 1)), rows.at(-1)!]
974
+ : rows.slice(0, maxLines);
975
+ return visibleRows.map((row, index) => {
976
+ const marker = index === visibleRows.length - 1 ? "└─" : "├─";
977
+ const prefix = row.prefix;
978
+ const text = row.text.startsWith("OWNER ") ? row.text.slice("OWNER ".length) : row.text;
979
+ return truncLine(theme.fg("dim", `${prefix}${marker} ${text}`), width);
980
+ });
853
981
  }
854
982
  const lines: string[] = [];
855
983
  const maxDepth = 2;
@@ -869,7 +997,8 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
869
997
  }
870
998
  const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
871
999
  const error = child.error ? ` · ${child.error}` : "";
872
- lines.push(theme.fg("dim", `${prefix}↳ ${nestedTimestampPrefix(formatClockTime(nestedRunEventTime(child)))}${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
1000
+ const modelThinking = formatModelThinking(child.model, child.thinking);
1001
+ lines.push(theme.fg("dim", `${prefix}↳ ${nestedTimestampPrefix(formatClockTime(nestedRunEventTime(child)))}${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state}${modelThinking ? ` · ${modelThinking}` : ""} · ${activity}${error}`));
873
1002
  if (depth === maxDepth) {
874
1003
  const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
875
1004
  if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
@@ -877,7 +1006,8 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
877
1006
  }
878
1007
  for (const step of child.steps ?? []) {
879
1008
  if (lines.length >= lineBudget) return;
880
- lines.push(theme.fg("dim", `${prefix} ↳ ${nestedTimestampPrefix(nestedStepTimestamp(step, child.lastUpdate))}${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
1009
+ const modelThinking = formatModelThinking(step.model, step.thinking);
1010
+ 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)}`));
881
1011
  append(step.children, depth + 1, `${prefix} `);
882
1012
  }
883
1013
  append(child.children, depth + 1, `${prefix} `);
@@ -896,14 +1026,15 @@ function foregroundStyleWidgetStepLines(
896
1026
  total: number,
897
1027
  expanded: boolean,
898
1028
  width: number,
1029
+ frame?: number,
899
1030
  ): string[] {
900
1031
  const status = widgetStepStatus(step.status, theme);
901
1032
  const stats = widgetStepStats(theme, step);
902
1033
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
903
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1034
+ 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}` : ""}`];
904
1035
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
905
1036
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
906
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
1037
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
907
1038
  lines.push(` ${nestedLine}`);
908
1039
  }
909
1040
  if (step.status === "running") {
@@ -926,40 +1057,40 @@ function foregroundStyleWidgetStepLines(
926
1057
  return lines;
927
1058
  }
928
1059
 
929
- function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
1060
+ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
930
1061
  if (!job.steps?.length) return [
931
1062
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
932
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
1063
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
933
1064
  ];
934
1065
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
935
1066
  const total = job.stepsTotal ?? job.steps.length;
936
1067
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
937
1068
  const lines: string[] = [];
938
1069
  for (const [index, step] of job.steps.entries()) {
939
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
1070
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, frame));
940
1071
  }
941
1072
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
942
1073
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
943
- for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
1074
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
944
1075
  lines.push(` ${nestedLine}`);
945
1076
  }
946
1077
  return lines;
947
1078
  }
948
1079
 
949
- function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
1080
+ function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
950
1081
  const stats = widgetStats(job, theme);
951
1082
  const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
952
1083
  const mode = widgetJobName(job);
953
1084
  const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
954
1085
  return [
955
1086
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
956
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
957
- ...foregroundStyleWidgetDetails(job, theme, expanded, width),
1087
+ `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1088
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width, frame),
958
1089
  ].map((line) => truncLine(line, width));
959
1090
  }
960
1091
 
961
- function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
962
- const fullLines = buildSingleWidgetLines(job, theme, width, false);
1092
+ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, frame?: number): string[] {
1093
+ const fullLines = buildSingleWidgetLines(job, theme, width, false, frame);
963
1094
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
964
1095
 
965
1096
  const total = job.stepsTotal ?? job.steps.length;
@@ -971,8 +1102,8 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
971
1102
  const stepStats = widgetStepStats(theme, step);
972
1103
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
973
1104
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
974
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
975
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
1105
+ 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}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1106
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
976
1107
  }
977
1108
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
978
1109
  return lines.map((line) => truncLine(line, width));
@@ -1027,10 +1158,10 @@ function widgetHeaderCounts(jobs: AsyncJobState[]): { running: AsyncJobState[];
1027
1158
  };
1028
1159
  }
1029
1160
 
1030
- function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number): string[] {
1161
+ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, frame?: number): string[] {
1031
1162
  const counts = widgetHeaderCounts(jobs);
1032
1163
  const hasActive = counts.running.length > 0 || counts.queued.length > 0;
1033
- const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
1164
+ const glyph = counts.running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(counts.running), frame)) : hasActive ? "●" : "○";
1034
1165
  const parts: string[] = [];
1035
1166
  if (counts.running.length > 0) parts.push(`${counts.running.length}/${jobs.length} running`);
1036
1167
  if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
@@ -1090,10 +1221,10 @@ function selectProgressiveJobKeys(jobs: AsyncJobState[], previousKeys: string[],
1090
1221
  return selected;
1091
1222
  }
1092
1223
 
1093
- function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: number): string {
1224
+ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: number, frame?: number): string {
1094
1225
  const counts = widgetHeaderCounts(jobs);
1095
1226
  const hasActive = counts.running.length > 0 || counts.queued.length > 0;
1096
- const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
1227
+ const glyph = counts.running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(counts.running), frame)) : hasActive ? "●" : "○";
1097
1228
  const parts: string[] = [];
1098
1229
  if (counts.running.length > 0) parts.push(formatAgentRunningLabel(counts.running.length));
1099
1230
  if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
@@ -1106,7 +1237,7 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
1106
1237
  return truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(", ") || `${jobs.length} total`)}`, width);
1107
1238
  }
1108
1239
 
1109
- function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number): string {
1240
+ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number): string {
1110
1241
  const stats = widgetStats(job, theme);
1111
1242
  const activity = widgetActivity(job);
1112
1243
  const status = job.status === "complete" ? "done" : job.status;
@@ -1116,7 +1247,7 @@ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number): st
1116
1247
  stats,
1117
1248
  activity && activity.toLowerCase() !== status ? theme.fg("dim", activity) : "",
1118
1249
  ].filter(Boolean);
1119
- return truncLine(` ${widgetStatusGlyph(job, theme)} ${parts.join(` ${theme.fg("dim", "·")} `)}`, width);
1250
+ return truncLine(` ${widgetStatusGlyph(job, theme, frame)} ${parts.join(` ${theme.fg("dim", "·")} `)}`, width);
1120
1251
  }
1121
1252
 
1122
1253
  function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width: number): string {
@@ -1129,9 +1260,9 @@ function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width:
1129
1260
  return truncLine(theme.fg("dim", ` +${hiddenJobs.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`), width);
1130
1261
  }
1131
1262
 
1132
- function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[]): { lines: string[]; visibleJobKeys: string[] } {
1263
+ function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[], frame?: number): { lines: string[]; visibleJobKeys: string[] } {
1133
1264
  const rowCount = Math.max(1, lockedRows);
1134
- if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width), visibleJobKeys: [] };
1265
+ if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width, frame), visibleJobKeys: [] };
1135
1266
 
1136
1267
  const bodyRows = rowCount - 1;
1137
1268
  let visibleJobKeys = selectProgressiveJobKeys(jobs, previousKeys, bodyRows);
@@ -1147,8 +1278,8 @@ function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
1147
1278
  }
1148
1279
 
1149
1280
  const lines = [
1150
- progressiveHeaderLine(jobs, theme, width),
1151
- ...visibleJobs.map((job) => progressiveJobLine(job, theme, width)),
1281
+ progressiveHeaderLine(jobs, theme, width, frame),
1282
+ ...visibleJobs.map((job) => progressiveJobLine(job, theme, width, frame)),
1152
1283
  ];
1153
1284
  if (hiddenJobs.length > 0 && lines.length < rowCount) lines.push(progressiveHiddenLine(hiddenJobs, theme, width));
1154
1285
  while (lines.length < rowCount) lines.push(" ");
@@ -1159,6 +1290,11 @@ function collapsedWidgetLineBudget(rows: number): number {
1159
1290
  return Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
1160
1291
  }
1161
1292
 
1293
+ function paddedWidgetLine(line: string, width: number): string {
1294
+ const text = ` ${line} `;
1295
+ return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
1296
+ }
1297
+
1162
1298
  function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
1163
1299
  const rows = process.stdout.rows || 30;
1164
1300
  const budget = expanded
@@ -1173,7 +1309,7 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
1173
1309
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
1174
1310
  }
1175
1311
 
1176
- function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
1312
+ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
1177
1313
  if (expanded) {
1178
1314
  resetWidgetLayoutSession();
1179
1315
  return fitWidgetLineBudget(lines, theme, width, true);
@@ -1185,11 +1321,11 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
1185
1321
  const availableRows = estimateAvailableWidgetRows();
1186
1322
 
1187
1323
  if (hasMatchingSession && widgetLayoutSession?.tier === "single-line") {
1188
- return buildSingleLineWidgetLines(jobs, theme, width);
1324
+ return buildSingleLineWidgetLines(jobs, theme, width, frame);
1189
1325
  }
1190
1326
 
1191
1327
  if (hasMatchingSession && widgetLayoutSession?.tier === "progressive" && widgetLayoutSession.lockedRows !== undefined) {
1192
- const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys);
1328
+ const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys, frame);
1193
1329
  widgetLayoutSession.visibleJobKeys = rendered.visibleJobKeys;
1194
1330
  return rendered.lines;
1195
1331
  }
@@ -1201,39 +1337,48 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
1201
1337
 
1202
1338
  if (availableRows <= 2) {
1203
1339
  widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
1204
- return buildSingleLineWidgetLines(jobs, theme, width);
1340
+ return buildSingleLineWidgetLines(jobs, theme, width, frame);
1205
1341
  }
1206
1342
 
1207
1343
  const lockedRows = Math.min(availableRows, collapsedWidgetLineBudget(rows));
1208
- const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, []);
1344
+ const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, [], frame);
1209
1345
  widgetLayoutSession = { expanded, rows, columns, tier: "progressive", lockedRows, visibleJobKeys: rendered.visibleJobKeys };
1210
1346
  return rendered.lines;
1211
1347
  }
1212
1348
 
1213
- function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
1214
- return (_tui, theme) => {
1215
- const width = getTermWidth();
1216
- const lines = expanded
1217
- ? buildWidgetLines(jobs, theme, width, true)
1218
- : jobs.length === 1
1219
- ? compactSingleWidgetLines(jobs[0]!, theme, width)
1220
- : buildWidgetLines(jobs, theme, width, false);
1349
+ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: { requestRender(): void }, theme: Theme) => Component & { dispose?(): void } {
1350
+ return (tui, theme) => {
1351
+ const requestRender = () => tui.requestRender();
1352
+ widgetRequestRender = requestRender;
1221
1353
  const container = new Container();
1222
- for (const line of fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
1223
- return container;
1354
+ container.render = (renderWidth: number): string[] => {
1355
+ const width = getTermWidth();
1356
+ const frame = widgetAnimationFrame();
1357
+ const lines = expanded
1358
+ ? buildWidgetLines(jobs, theme, width, true, frame)
1359
+ : jobs.length === 1
1360
+ ? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
1361
+ : buildWidgetLines(jobs, theme, width, false, frame);
1362
+ return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
1363
+ };
1364
+ const component = container as Component & { dispose?(): void };
1365
+ component.dispose = () => {
1366
+ if (widgetRequestRender === requestRender) widgetRequestRender = undefined;
1367
+ };
1368
+ return component;
1224
1369
  };
1225
1370
  }
1226
1371
 
1227
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
1372
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
1228
1373
  if (jobs.length === 0) return [];
1229
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
1374
+ if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame);
1230
1375
  const running = jobs.filter((job) => job.status === "running");
1231
1376
  const queued = jobs.filter((job) => job.status === "queued");
1232
1377
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
1233
1378
 
1234
1379
  const lines: string[] = [];
1235
1380
  const hasActive = running.length > 0 || queued.length > 0;
1236
- const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
1381
+ const headerGlyph = running.length > 0 ? runningGlyph(animatedSeed(widgetJobsRunningSeed(running), frame)) : hasActive ? "●" : "○";
1237
1382
  lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
1238
1383
 
1239
1384
  const items: string[][] = [];
@@ -1246,7 +1391,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1246
1391
  if (slots <= 0) { hiddenRunning++; continue; }
1247
1392
  const stats = widgetStats(job, theme);
1248
1393
  items.push([
1249
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1394
+ `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1250
1395
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1251
1396
  ...widgetParallelAgentDetails(job, theme, expanded, width),
1252
1397
  ]);
@@ -1263,7 +1408,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1263
1408
  if (slots <= 0) { hiddenFinished++; continue; }
1264
1409
  const stats = widgetStats(job, theme);
1265
1410
  items.push([
1266
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1411
+ `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1267
1412
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1268
1413
  ...widgetParallelAgentDetails(job, theme, expanded, width),
1269
1414
  ]);
@@ -1300,6 +1445,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1300
1445
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
1301
1446
  if (jobs.length === 0) {
1302
1447
  resetWidgetLayoutSession();
1448
+ widgetRequestRender = undefined;
1303
1449
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1304
1450
  return;
1305
1451
  }
@@ -1310,7 +1456,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1310
1456
  function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
1311
1457
  const output = r.truncation?.text || getSingleResultOutput(r);
1312
1458
  const progress = r.progress || r.progressSummary;
1313
- const isRunning = r.progress?.status === "running";
1459
+ const isRunning = isResultRunning(r);
1314
1460
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1315
1461
  const stats = statJoin(theme, [
1316
1462
  r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
@@ -1349,16 +1495,64 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1349
1495
  return c;
1350
1496
  }
1351
1497
 
1498
+ function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1499
+ if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1500
+ if (row.state === "complete") return theme.fg("success", "✓");
1501
+ return theme.fg("error", "✗");
1502
+ }
1503
+
1504
+ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): string {
1505
+ const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1506
+ if (row.state === "running") return theme.fg("accent", label);
1507
+ if (row.state === "complete") return theme.fg("success", label);
1508
+ return theme.fg("error", label);
1509
+ }
1510
+
1511
+ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" {
1512
+ if (isError || rows.some((row) => row.state === "failed")) return "failed";
1513
+ if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
1514
+ return "running";
1515
+ }
1516
+
1517
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, frame?: number): Component {
1518
+ const workflow = d.workflow;
1519
+ const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1520
+ const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1521
+ const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : theme.fg("error", "✗");
1522
+ const width = getTermWidth() - 4;
1523
+ const runId = d.runId ? d.runId.slice(0, 12) : "workflow";
1524
+ const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
1525
+ const phase = rows.find((row) => row.state === "running" && row.phase)?.phase ?? [...rows].reverse().find((row) => row.phase)?.phase;
1526
+ const c = new Container();
1527
+ 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));
1528
+ c.addChild(new Text(truncLine(theme.fg("dim", ` Repo ${repoLabel}`), width), 0, 0));
1529
+ if (phase) c.addChild(new Text(truncLine(theme.fg("dim", ` Phase ${phase}`), width), 0, 0));
1530
+ if (rows.length === 0) {
1531
+ c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
1532
+ return c;
1533
+ }
1534
+ for (const row of rows) {
1535
+ const status = workflowRowStateLabel(row, theme);
1536
+ const label = row.label && row.label !== row.key ? ` ${row.label}` : "";
1537
+ const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1538
+ const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1539
+ const error = row.error ? ` ${theme.fg("error", `· ${row.error}`)}` : "";
1540
+ c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1541
+ }
1542
+ if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
1543
+ return c;
1544
+ }
1545
+
1352
1546
  function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component {
1353
- const hasRunning = d.progress?.some((p) => p.status === "running")
1354
- || d.results.some((r) => r.progress?.status === "running")
1355
- || workflowGraphHasStatus(d, ["running"]);
1356
- const stopped = d.results.some((r) => r.stopped && r.progress?.status !== "running")
1547
+ const hasRunning = detailsHaveRunningResult(d);
1548
+ const detached = d.results.some((r) => r.detached)
1549
+ || workflowGraphHasStatus(d, ["detached"]);
1550
+ const stopped = d.results.some((r) => r.stopped)
1357
1551
  || workflowGraphHasStatus(d, ["stopped"]);
1358
- const failed = d.results.some((r) => !r.stopped && r.exitCode !== 0 && r.progress?.status !== "running")
1552
+ const failed = d.results.some((r) => !hasTerminalResultFlag(r) && r.exitCode !== 0 && !isResultRunning(r))
1359
1553
  || workflowGraphHasStatus(d, ["failed"]);
1360
- const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
1361
- || workflowGraphHasStatus(d, ["paused", "detached"]);
1554
+ const paused = d.results.some((r) => r.interrupted)
1555
+ || workflowGraphHasStatus(d, ["paused"]);
1362
1556
  let totalSummary = d.progressSummary;
1363
1557
  if (!totalSummary) {
1364
1558
  let sawProgress = false;
@@ -1376,15 +1570,16 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1376
1570
  const multiLabel = buildMultiProgressLabel(d, hasRunning);
1377
1571
  const itemTitle = multiLabel.itemTitle;
1378
1572
  const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary), formatTotalCostStat(d.totalCost)]);
1379
- const glyph = hasRunning
1380
- ? theme.fg("accent", runningGlyph(frame !== undefined ? (runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex) ?? 0) + frame : runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
1381
- : stopped
1382
- ? theme.fg("warning", "■")
1383
- : failed
1384
- ? theme.fg("error", "✗")
1385
- : paused
1386
- ? theme.fg("warning", "■")
1387
- : theme.fg("success", "✓");
1573
+ const aggregatePresentation = semanticResultPresentation({
1574
+ running: hasRunning,
1575
+ detached,
1576
+ stopped,
1577
+ interrupted: paused,
1578
+ failed,
1579
+ seed: runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex),
1580
+ frame,
1581
+ });
1582
+ const glyph = theme.fg(aggregatePresentation.tone, aggregatePresentation.glyph);
1388
1583
  const contextBadge = contextModeBadge(theme, d.context);
1389
1584
  const c = new Container();
1390
1585
  const width = getTermWidth() - 4;
@@ -1421,7 +1616,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1421
1616
  const output = getSingleResultOutput(r);
1422
1617
  const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1423
1618
  const rProg = r.progress || progressFromArray || r.progressSummary;
1424
- const rRunning = rProg && "status" in rProg && rProg.status === "running";
1619
+ const rRunning = rProg && "status" in rProg && isResultRunning(r, rProg.status);
1425
1620
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
1426
1621
  const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
1427
1622
  const stepStats = formatProgressStats(theme, rProg);
@@ -1433,9 +1628,10 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1433
1628
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1434
1629
  c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1435
1630
  if (rRunning && rProg && "status" in rProg) {
1436
- const activity = compactCurrentActivity(rProg);
1631
+ const liveProgress = rProg as AgentProgress;
1632
+ const activity = compactCurrentActivity(liveProgress);
1437
1633
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1438
- for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(rProg))) {
1634
+ for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(liveProgress))) {
1439
1635
  c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1440
1636
  }
1441
1637
  c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
@@ -1455,6 +1651,45 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1455
1651
  return c;
1456
1652
  }
1457
1653
 
1654
+ export function renderSubagentSummary(
1655
+ result: AgentToolResult<Details>,
1656
+ options: { isPartial?: boolean },
1657
+ theme: Theme,
1658
+ ): Component {
1659
+ const details = result.details;
1660
+ const results = details?.results ?? [];
1661
+ const hasSingleTerminalResult = results.length === 1 && hasTerminalResult(results[0]!);
1662
+ const hasOnlyTerminalResults = results.length > 0 && results.every(hasTerminalResult);
1663
+ const running = !hasSingleTerminalResult && !hasOnlyTerminalResults && (
1664
+ options.isPartial === true
1665
+ || Boolean(details?.asyncId && details.mode !== "management")
1666
+ || Boolean(details && detailsHaveRunningResult(details))
1667
+ );
1668
+ const stopped = results.some((entry) => entry.stopped)
1669
+ || Boolean(details && workflowGraphHasStatus(details, ["stopped"]));
1670
+ const paused = results.some((entry) => entry.interrupted || entry.detached)
1671
+ || Boolean(details && workflowGraphHasStatus(details, ["paused", "detached"]));
1672
+ const failed = result.isError === true
1673
+ || results.some((entry) => !hasTerminalResultFlag(entry) && entry.exitCode !== 0 && !isResultRunning(entry))
1674
+ || Boolean(details && workflowGraphHasStatus(details, ["failed"]));
1675
+ const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : "completed";
1676
+ const glyph = state === "running"
1677
+ ? theme.fg("accent", STATIC_RUNNING_GLYPH)
1678
+ : state === "completed"
1679
+ ? theme.fg("success", "✓")
1680
+ : state === "failed"
1681
+ ? theme.fg("error", "✗")
1682
+ : theme.fg("warning", "■");
1683
+ const label = details?.mode === "single" && results.length === 1
1684
+ ? results[0]?.agent || "subagent"
1685
+ : details?.mode || "subagent";
1686
+ return new Text(
1687
+ 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),
1688
+ 0,
1689
+ 0,
1690
+ );
1691
+ }
1692
+
1458
1693
  /**
1459
1694
  * Render a subagent result
1460
1695
  */
@@ -1465,6 +1700,7 @@ export function renderSubagentResult(
1465
1700
  frame?: number,
1466
1701
  ): Component {
1467
1702
  const d = result.details;
1703
+ if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) return renderWorkflowChatProgress(d, result, theme, frame);
1468
1704
  if (!d || !d.results.length) {
1469
1705
  const t = result.content[0];
1470
1706
  const text = t?.type === "text" ? t.text : "(no output)";
@@ -1490,17 +1726,12 @@ export function renderSubagentResult(
1490
1726
 
1491
1727
  if (d.mode === "single" && d.results.length === 1) {
1492
1728
  const r = d.results[0];
1729
+ if (!r) return renderMultiCompact(d, theme, frame);
1493
1730
  if (!expanded) return renderSingleCompact(d, r, theme, frame);
1494
- const isRunning = r.progress?.status === "running";
1495
- const icon = isRunning
1496
- ? theme.fg("warning", "running")
1497
- : r.detached
1498
- ? theme.fg("warning", "detached")
1499
- : r.exitCode === 0
1500
- ? theme.fg("success", "ok")
1501
- : theme.fg("error", "failed");
1731
+ const isRunning = isResultRunning(r);
1502
1732
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1503
1733
  const output = r.truncation?.text || getSingleResultOutput(r);
1734
+ const presentation = styledResultPresentation(resultPresentation(r, output, isRunning, undefined, frame), theme);
1504
1735
 
1505
1736
  const progressInfo = isRunning && r.progress
1506
1737
  ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
@@ -1512,7 +1743,7 @@ export function renderSubagentResult(
1512
1743
  const fit = (text: string) => expanded ? text : truncLine(text, w);
1513
1744
  const toolCallLines = getToolCallLines(r, expanded);
1514
1745
  const c = new Container();
1515
- c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
1746
+ c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
1516
1747
  c.addChild(new Spacer(1));
1517
1748
  const taskMaxLen = Math.max(20, w - 8);
1518
1749
  const taskPreview = expanded || r.task.length <= taskMaxLen
@@ -1593,31 +1824,30 @@ export function renderSubagentResult(
1593
1824
 
1594
1825
  if (!expanded) return renderMultiCompact(d, theme, frame);
1595
1826
 
1596
- const hasRunning = d.progress?.some((p) => p.status === "running")
1597
- || d.results.some((r) => r.progress?.status === "running")
1598
- || workflowGraphHasStatus(d, ["running"]);
1599
- const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
1600
- const hasEmptyWithoutTarget = d.results.some((r) =>
1601
- r.exitCode === 0
1602
- && r.progress?.status !== "running"
1827
+ const hasRunning = detailsHaveRunningResult(d);
1828
+ const detached = d.results.some((r) => r.detached)
1829
+ || workflowGraphHasStatus(d, ["detached"]);
1830
+ const stopped = d.results.some((r) => r.stopped)
1831
+ || workflowGraphHasStatus(d, ["stopped"]);
1832
+ const failed = d.results.some((r) => !hasTerminalResultFlag(r) && r.exitCode !== 0 && !isResultRunning(r))
1833
+ || workflowGraphHasStatus(d, ["failed"]);
1834
+ const paused = d.results.some((r) => r.interrupted)
1835
+ || workflowGraphHasStatus(d, ["paused"]);
1836
+ const completedWithoutOutput = d.results.some((r) =>
1837
+ !hasTerminalResultFlag(r)
1838
+ && r.exitCode === 0
1839
+ && !isResultRunning(r)
1603
1840
  && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1604
1841
  );
1605
- const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
1606
- const hasWorkflowStop = d.results.some((r) => r.stopped && r.progress?.status !== "running") || workflowGraphHasStatus(d, ["stopped"]);
1607
- const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
1608
- const icon = hasRunning
1609
- ? theme.fg("warning", "running")
1610
- : hasEmptyWithoutTarget
1611
- ? theme.fg("warning", "warning")
1612
- : hasWorkflowFailure
1613
- ? theme.fg("error", "failed")
1614
- : hasWorkflowStop
1615
- ? theme.fg("warning", "stopped")
1616
- : hasWorkflowPause
1617
- ? theme.fg("warning", "paused")
1618
- : ok === d.results.length
1619
- ? theme.fg("success", "ok")
1620
- : theme.fg("error", "failed");
1842
+ const presentation = styledResultPresentation(semanticResultPresentation({
1843
+ running: hasRunning,
1844
+ detached,
1845
+ stopped,
1846
+ interrupted: paused,
1847
+ failed,
1848
+ completedWithoutOutput,
1849
+ frame,
1850
+ }), theme);
1621
1851
 
1622
1852
  const totalSummary =
1623
1853
  d.progressSummary ||
@@ -1654,22 +1884,14 @@ export function renderSubagentResult(
1654
1884
  ? d.chainAgents
1655
1885
  .map((agent, i) => {
1656
1886
  const result = d.results[i];
1657
- const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
1658
- const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
1659
- const isEmptyWithoutTarget = Boolean(result)
1660
- && Boolean(isComplete)
1661
- && hasEmptyTextOutputWithoutOutputTarget(result.task, getSingleResultOutput(result));
1662
1887
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
1663
- const stepIcon = isFailed
1664
- ? theme.fg("error", "failed")
1665
- : isEmptyWithoutTarget
1666
- ? theme.fg("warning", "warning")
1667
- : isComplete
1668
- ? theme.fg("success", "done")
1669
- : isCurrent && hasRunning
1670
- ? theme.fg("warning", "running")
1671
- : theme.fg("dim", "pending");
1672
- return `${stepIcon} ${agent}${contextModeBadge(theme, result?.context)}`;
1888
+ const stepPresentation = result
1889
+ ? styledResultPresentation(resultPresentation(result, getSingleResultOutput(result), isCurrent && hasRunning && !hasTerminalResultFlag(result)), theme)
1890
+ : undefined;
1891
+ const stepStatus = stepPresentation
1892
+ ? `${stepPresentation.glyph} ${stepPresentation.label}`
1893
+ : theme.fg("dim", "◦ pending");
1894
+ return `${stepStatus} ${agent}${contextModeBadge(theme, result?.context)}`;
1673
1895
  })
1674
1896
  .join(theme.fg("dim", " → "))
1675
1897
  : null;
@@ -1679,7 +1901,7 @@ export function renderSubagentResult(
1679
1901
  const c = new Container();
1680
1902
  c.addChild(
1681
1903
  new Text(
1682
- fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr}`),
1904
+ fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr} ${theme.fg("dim", "·")} ${presentation.label}`),
1683
1905
  0,
1684
1906
  0,
1685
1907
  ),
@@ -1726,24 +1948,18 @@ export function renderSubagentResult(
1726
1948
  const progressFromArray = d.progress?.find((p) => p.index === i)
1727
1949
  || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1728
1950
  const rProg = r.progress || progressFromArray || r.progressSummary;
1729
- const rRunning = rProg?.status === "running";
1951
+ const rRunning = isResultRunning(r, rProg?.status);
1730
1952
  const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
1731
1953
 
1732
1954
  const resultOutput = getSingleResultOutput(r);
1733
- const statusIcon = rRunning
1734
- ? theme.fg("warning", "running")
1735
- : r.exitCode !== 0
1736
- ? theme.fg("error", "failed")
1737
- : hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
1738
- ? theme.fg("warning", "warning")
1739
- : theme.fg("success", "done");
1955
+ const rowPresentation = styledResultPresentation(resultPresentation(r, resultOutput, rRunning, progressRunningSeed(rProg), frame), theme);
1740
1956
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
1741
- const modelDisplay = modelThinkingBadge(theme, r.model);
1957
+ const modelDisplay = modelThinkingBadge(theme, r.model ?? rProg?.model, r.thinking ?? rProg?.thinking);
1742
1958
  const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
1743
1959
  const contextBadge = contextModeBadge(theme, r.context);
1744
1960
  const stepHeader = rRunning
1745
- ? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats}`
1746
- : `${statusIcon} ${stepLabel}: ${theme.bold(r.agent)}${contextBadge}${modelDisplay}${stats}`;
1961
+ ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
1962
+ : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(r.agent)}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
1747
1963
  const toolCallLines = getToolCallLines(r, expanded);
1748
1964
  c.addChild(new Text(fit(stepHeader), 0, 0));
1749
1965