pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. 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
 
@@ -277,17 +278,81 @@ function resultStatusLine(result: Details["results"][number], output: string): s
277
278
  return "Done";
278
279
  }
279
280
 
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));
281
+ type ResultPresentation = {
282
+ glyph: string;
283
+ label: "running" | "detached" | "stopped" | "paused" | "failed" | "completed";
284
+ tone: "accent" | "warning" | "error" | "success";
285
+ };
286
+
287
+ function semanticResultPresentation(input: {
288
+ running?: boolean;
289
+ detached?: boolean;
290
+ stopped?: boolean;
291
+ interrupted?: boolean;
292
+ failed?: boolean;
293
+ completedWithoutOutput?: boolean;
294
+ seed?: number;
295
+ frame?: number;
296
+ }): ResultPresentation {
297
+ if (input.running) {
298
+ const glyph = input.frame !== undefined ? runningGlyph((input.seed ?? 0) + input.frame) : runningGlyph(input.seed);
299
+ return { glyph, label: "running", tone: "accent" };
284
300
  }
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", "✓");
301
+ if (input.detached) return { glyph: "■", label: "detached", tone: "warning" };
302
+ if (input.stopped) return { glyph: "■", label: "stopped", tone: "warning" };
303
+ if (input.interrupted) return { glyph: "■", label: "paused", tone: "warning" };
304
+ if (input.failed) return { glyph: "✗", label: "failed", tone: "error" };
305
+ return { glyph: "✓", label: "completed", tone: input.completedWithoutOutput ? "warning" : "success" };
306
+ }
307
+
308
+ function hasTerminalResultFlag(result: Details["results"][number]): boolean {
309
+ return Boolean(result.detached || result.stopped || result.interrupted);
310
+ }
311
+
312
+ function hasTerminalResult(result: Details["results"][number]): boolean {
313
+ if (hasTerminalResultFlag(result)) return true;
314
+ const status = result.progress?.status;
315
+ if (status === "running" || status === "pending") return false;
316
+ return result.exitCode !== undefined;
317
+ }
318
+
319
+ function isResultRunning(result: Details["results"][number], status = result.progress?.status): boolean {
320
+ return status === "running" && !hasTerminalResultFlag(result);
321
+ }
322
+
323
+ function detailsHaveRunningResult(details: Details): boolean {
324
+ return details.progress?.some((progress) => {
325
+ if (progress.status !== "running") return false;
326
+ const result = details.results.find((entry) => entry.progress?.index === progress.index) ?? details.results[progress.index];
327
+ return !result || !hasTerminalResultFlag(result);
328
+ })
329
+ || details.results.some((result) => isResultRunning(result))
330
+ || workflowGraphHasStatus(details, ["running"]);
331
+ }
332
+
333
+ function resultPresentation(result: Details["results"][number], output: string, running = isResultRunning(result), seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): ResultPresentation {
334
+ return semanticResultPresentation({
335
+ running,
336
+ detached: result.detached,
337
+ stopped: result.stopped,
338
+ interrupted: result.interrupted,
339
+ failed: result.exitCode !== 0,
340
+ completedWithoutOutput: hasEmptyTextOutputWithoutOutputTarget(result.task, output),
341
+ seed,
342
+ frame,
343
+ });
344
+ }
345
+
346
+ function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = isResultRunning(result), seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): string {
347
+ const presentation = resultPresentation(result, output, running, seed, frame);
348
+ return theme.fg(presentation.tone, presentation.glyph);
349
+ }
350
+
351
+ function styledResultPresentation(presentation: ResultPresentation, theme: Theme): { glyph: string; label: string } {
352
+ return {
353
+ glyph: theme.fg(presentation.tone, presentation.glyph),
354
+ label: theme.fg(presentation.tone, presentation.label),
355
+ };
291
356
  }
292
357
 
293
358
  function compactCurrentActivity(progress: AgentProgress): string {
@@ -471,7 +536,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
471
536
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
472
537
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
473
538
  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}`);
539
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
475
540
  }
476
541
  return lines;
477
542
  }
@@ -639,14 +704,12 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
639
704
  || details.progress?.find((progress) => progress.agent === result.agent && progress.status === "running");
640
705
  const index = result.progress?.index ?? progressFromArray?.index ?? i;
641
706
  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");
707
+ const status = result.stopped
708
+ ? "stopped"
709
+ : result.interrupted || result.detached
710
+ ? "detached"
711
+ : result.progress?.status
712
+ ?? (result.exitCode === 0 ? "completed" : "failed");
650
713
  statuses[index] = status;
651
714
  }
652
715
  const running = statuses.filter((status) => status === "running").length;
@@ -667,8 +730,8 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
667
730
  let done = 0;
668
731
  for (let index = groupStart; index < groupEnd; index++) {
669
732
  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") {
733
+ const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
734
+ if (progressEntry?.status === "running" && (!resultEntry || !hasTerminalResultFlag(resultEntry))) {
672
735
  running++;
673
736
  continue;
674
737
  }
@@ -847,9 +910,56 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
847
910
  function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
848
911
  if (!children?.length || lineBudget <= 0) return [];
849
912
  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}`)] : [];
913
+ type CollapsedRow = { text: string; prefix: string };
914
+ const rows: CollapsedRow[] = [];
915
+ const maxLeaves = 4;
916
+ const maxLines = Math.min(6, lineBudget);
917
+ let leaves = 0;
918
+ let overflow = 0;
919
+ const appendLeaf = (step: NestedStepSummary | NestedRunSummary, prefix: string, fallback?: number): void => {
920
+ if (leaves >= maxLeaves) {
921
+ overflow++;
922
+ return;
923
+ }
924
+ leaves++;
925
+ const state = "status" in step ? step.status : step.state;
926
+ const modelThinking = formatModelThinking(step.model, step.thinking);
927
+ const activity = nestedActivity(step, state, snapshotNow ?? fallback);
928
+ const timestamp = "status" in step ? nestedStepTimestamp(step, fallback) : formatClockTime(nestedRunEventTime(step));
929
+ const error = step.error ? ` · ${step.error}` : "";
930
+ const name = "status" in step ? step.agent : nestedRunName(step);
931
+ rows.push({
932
+ prefix,
933
+ text: `${nestedTimestampPrefix(timestamp)}${nestedStatusGlyph(state, theme)} ${name} · ${state}${modelThinking ? ` · ${modelThinking}` : ""}${activity ? ` · ${activity}` : ""}${error}`,
934
+ });
935
+ };
936
+ for (const child of children) {
937
+ const steps = (child.mode === "parallel" || child.mode === "chain") ? child.steps ?? [] : [];
938
+ if (steps.length > 0) {
939
+ const ownerModelThinking = formatModelThinking(child.model, child.thinking);
940
+ const ownerActivity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
941
+ const ownerError = child.error ? ` · ${child.error}` : "";
942
+ rows.push({
943
+ prefix: "↳ ",
944
+ text: `OWNER ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state}${ownerModelThinking ? ` · ${ownerModelThinking}` : ""}${ownerActivity ? ` · ${ownerActivity}` : ""}${ownerError}`,
945
+ });
946
+ for (const step of steps) appendLeaf(step, "↳ │ ", child.lastUpdate);
947
+ } else {
948
+ appendLeaf(child, "↳ ", child.lastUpdate);
949
+ }
950
+ }
951
+ if (overflow > 0) rows.push({ prefix: "↳ ", text: `… +${overflow} more nested leaves` });
952
+ const visibleRows = rows.length <= maxLines
953
+ ? rows
954
+ : overflow > 0
955
+ ? [...rows.slice(0, Math.max(0, maxLines - 1)), rows.at(-1)!]
956
+ : rows.slice(0, maxLines);
957
+ return visibleRows.map((row, index) => {
958
+ const marker = index === visibleRows.length - 1 ? "└─" : "├─";
959
+ const prefix = row.prefix;
960
+ const text = row.text.startsWith("OWNER ") ? row.text.slice("OWNER ".length) : row.text;
961
+ return truncLine(theme.fg("dim", `${prefix}${marker} ${text}`), width);
962
+ });
853
963
  }
854
964
  const lines: string[] = [];
855
965
  const maxDepth = 2;
@@ -869,7 +979,8 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
869
979
  }
870
980
  const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
871
981
  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}`));
982
+ const modelThinking = formatModelThinking(child.model, child.thinking);
983
+ 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
984
  if (depth === maxDepth) {
874
985
  const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
875
986
  if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
@@ -877,7 +988,8 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
877
988
  }
878
989
  for (const step of child.steps ?? []) {
879
990
  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)}`));
991
+ const modelThinking = formatModelThinking(step.model, step.thinking);
992
+ 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
993
  append(step.children, depth + 1, `${prefix} `);
882
994
  }
883
995
  append(child.children, depth + 1, `${prefix} `);
@@ -903,7 +1015,7 @@ function foregroundStyleWidgetStepLines(
903
1015
  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}` : ""}`];
904
1016
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
905
1017
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
906
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
1018
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
907
1019
  lines.push(` ${nestedLine}`);
908
1020
  }
909
1021
  if (step.status === "running") {
@@ -929,7 +1041,7 @@ function foregroundStyleWidgetStepLines(
929
1041
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
930
1042
  if (!job.steps?.length) return [
931
1043
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
932
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
1044
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
933
1045
  ];
934
1046
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
935
1047
  const total = job.stepsTotal ?? job.steps.length;
@@ -940,7 +1052,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
940
1052
  }
941
1053
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
942
1054
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
943
- for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
1055
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
944
1056
  lines.push(` ${nestedLine}`);
945
1057
  }
946
1058
  return lines;
@@ -972,7 +1084,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
972
1084
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
973
1085
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
974
1086
  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}`);
1087
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
976
1088
  }
977
1089
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
978
1090
  return lines.map((line) => truncLine(line, width));
@@ -1310,7 +1422,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1310
1422
  function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
1311
1423
  const output = r.truncation?.text || getSingleResultOutput(r);
1312
1424
  const progress = r.progress || r.progressSummary;
1313
- const isRunning = r.progress?.status === "running";
1425
+ const isRunning = isResultRunning(r);
1314
1426
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1315
1427
  const stats = statJoin(theme, [
1316
1428
  r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
@@ -1349,16 +1461,64 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1349
1461
  return c;
1350
1462
  }
1351
1463
 
1464
+ function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1465
+ if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1466
+ if (row.state === "complete") return theme.fg("success", "✓");
1467
+ return theme.fg("error", "✗");
1468
+ }
1469
+
1470
+ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): string {
1471
+ const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1472
+ if (row.state === "running") return theme.fg("accent", label);
1473
+ if (row.state === "complete") return theme.fg("success", label);
1474
+ return theme.fg("error", label);
1475
+ }
1476
+
1477
+ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" {
1478
+ if (isError || rows.some((row) => row.state === "failed")) return "failed";
1479
+ if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
1480
+ return "running";
1481
+ }
1482
+
1483
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, frame?: number): Component {
1484
+ const workflow = d.workflow;
1485
+ const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1486
+ const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
1487
+ const glyph = state === "running" ? theme.fg("accent", runningGlyph(frame)) : state === "complete" ? theme.fg("success", "✓") : theme.fg("error", "✗");
1488
+ const width = getTermWidth() - 4;
1489
+ const runId = d.runId ? d.runId.slice(0, 12) : "workflow";
1490
+ const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
1491
+ const phase = rows.find((row) => row.state === "running" && row.phase)?.phase ?? [...rows].reverse().find((row) => row.phase)?.phase;
1492
+ const c = new Container();
1493
+ 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));
1494
+ c.addChild(new Text(truncLine(theme.fg("dim", ` Repo ${repoLabel}`), width), 0, 0));
1495
+ if (phase) c.addChild(new Text(truncLine(theme.fg("dim", ` Phase ${phase}`), width), 0, 0));
1496
+ if (rows.length === 0) {
1497
+ c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
1498
+ return c;
1499
+ }
1500
+ for (const row of rows) {
1501
+ const status = workflowRowStateLabel(row, theme);
1502
+ const label = row.label && row.label !== row.key ? ` ${row.label}` : "";
1503
+ const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1504
+ const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1505
+ const error = row.error ? ` ${theme.fg("error", `· ${row.error}`)}` : "";
1506
+ c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1507
+ }
1508
+ if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
1509
+ return c;
1510
+ }
1511
+
1352
1512
  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")
1513
+ const hasRunning = detailsHaveRunningResult(d);
1514
+ const detached = d.results.some((r) => r.detached)
1515
+ || workflowGraphHasStatus(d, ["detached"]);
1516
+ const stopped = d.results.some((r) => r.stopped)
1357
1517
  || workflowGraphHasStatus(d, ["stopped"]);
1358
- const failed = d.results.some((r) => !r.stopped && r.exitCode !== 0 && r.progress?.status !== "running")
1518
+ const failed = d.results.some((r) => !hasTerminalResultFlag(r) && r.exitCode !== 0 && !isResultRunning(r))
1359
1519
  || workflowGraphHasStatus(d, ["failed"]);
1360
- const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
1361
- || workflowGraphHasStatus(d, ["paused", "detached"]);
1520
+ const paused = d.results.some((r) => r.interrupted)
1521
+ || workflowGraphHasStatus(d, ["paused"]);
1362
1522
  let totalSummary = d.progressSummary;
1363
1523
  if (!totalSummary) {
1364
1524
  let sawProgress = false;
@@ -1376,15 +1536,16 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1376
1536
  const multiLabel = buildMultiProgressLabel(d, hasRunning);
1377
1537
  const itemTitle = multiLabel.itemTitle;
1378
1538
  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", "✓");
1539
+ const aggregatePresentation = semanticResultPresentation({
1540
+ running: hasRunning,
1541
+ detached,
1542
+ stopped,
1543
+ interrupted: paused,
1544
+ failed,
1545
+ seed: runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex),
1546
+ frame,
1547
+ });
1548
+ const glyph = theme.fg(aggregatePresentation.tone, aggregatePresentation.glyph);
1388
1549
  const contextBadge = contextModeBadge(theme, d.context);
1389
1550
  const c = new Container();
1390
1551
  const width = getTermWidth() - 4;
@@ -1421,7 +1582,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1421
1582
  const output = getSingleResultOutput(r);
1422
1583
  const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1423
1584
  const rProg = r.progress || progressFromArray || r.progressSummary;
1424
- const rRunning = rProg && "status" in rProg && rProg.status === "running";
1585
+ const rRunning = rProg && "status" in rProg && isResultRunning(r, rProg.status);
1425
1586
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
1426
1587
  const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
1427
1588
  const stepStats = formatProgressStats(theme, rProg);
@@ -1433,9 +1594,10 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1433
1594
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1434
1595
  c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1435
1596
  if (rRunning && rProg && "status" in rProg) {
1436
- const activity = compactCurrentActivity(rProg);
1597
+ const liveProgress = rProg as AgentProgress;
1598
+ const activity = compactCurrentActivity(liveProgress);
1437
1599
  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))) {
1600
+ for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(liveProgress))) {
1439
1601
  c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1440
1602
  }
1441
1603
  c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
@@ -1455,6 +1617,45 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1455
1617
  return c;
1456
1618
  }
1457
1619
 
1620
+ export function renderSubagentSummary(
1621
+ result: AgentToolResult<Details>,
1622
+ options: { isPartial?: boolean },
1623
+ theme: Theme,
1624
+ ): Component {
1625
+ const details = result.details;
1626
+ const results = details?.results ?? [];
1627
+ const hasSingleTerminalResult = results.length === 1 && hasTerminalResult(results[0]!);
1628
+ const hasOnlyTerminalResults = results.length > 0 && results.every(hasTerminalResult);
1629
+ const running = !hasSingleTerminalResult && !hasOnlyTerminalResults && (
1630
+ options.isPartial === true
1631
+ || Boolean(details?.asyncId && details.mode !== "management")
1632
+ || Boolean(details && detailsHaveRunningResult(details))
1633
+ );
1634
+ const stopped = results.some((entry) => entry.stopped)
1635
+ || Boolean(details && workflowGraphHasStatus(details, ["stopped"]));
1636
+ const paused = results.some((entry) => entry.interrupted || entry.detached)
1637
+ || Boolean(details && workflowGraphHasStatus(details, ["paused", "detached"]));
1638
+ const failed = result.isError === true
1639
+ || results.some((entry) => !hasTerminalResultFlag(entry) && entry.exitCode !== 0 && !isResultRunning(entry))
1640
+ || Boolean(details && workflowGraphHasStatus(details, ["failed"]));
1641
+ const state = running ? "running" : failed ? "failed" : stopped ? "stopped" : paused ? "paused" : "completed";
1642
+ const glyph = state === "running"
1643
+ ? theme.fg("accent", STATIC_RUNNING_GLYPH)
1644
+ : state === "completed"
1645
+ ? theme.fg("success", "✓")
1646
+ : state === "failed"
1647
+ ? theme.fg("error", "✗")
1648
+ : theme.fg("warning", "■");
1649
+ const label = details?.mode === "single" && results.length === 1
1650
+ ? results[0]?.agent || "subagent"
1651
+ : details?.mode || "subagent";
1652
+ return new Text(
1653
+ 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),
1654
+ 0,
1655
+ 0,
1656
+ );
1657
+ }
1658
+
1458
1659
  /**
1459
1660
  * Render a subagent result
1460
1661
  */
@@ -1465,6 +1666,7 @@ export function renderSubagentResult(
1465
1666
  frame?: number,
1466
1667
  ): Component {
1467
1668
  const d = result.details;
1669
+ if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) return renderWorkflowChatProgress(d, result, theme, frame);
1468
1670
  if (!d || !d.results.length) {
1469
1671
  const t = result.content[0];
1470
1672
  const text = t?.type === "text" ? t.text : "(no output)";
@@ -1490,17 +1692,12 @@ export function renderSubagentResult(
1490
1692
 
1491
1693
  if (d.mode === "single" && d.results.length === 1) {
1492
1694
  const r = d.results[0];
1695
+ if (!r) return renderMultiCompact(d, theme, frame);
1493
1696
  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");
1697
+ const isRunning = isResultRunning(r);
1502
1698
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1503
1699
  const output = r.truncation?.text || getSingleResultOutput(r);
1700
+ const presentation = styledResultPresentation(resultPresentation(r, output, isRunning, undefined, frame), theme);
1504
1701
 
1505
1702
  const progressInfo = isRunning && r.progress
1506
1703
  ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
@@ -1512,7 +1709,7 @@ export function renderSubagentResult(
1512
1709
  const fit = (text: string) => expanded ? text : truncLine(text, w);
1513
1710
  const toolCallLines = getToolCallLines(r, expanded);
1514
1711
  const c = new Container();
1515
- c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
1712
+ c.addChild(new Text(fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo} ${theme.fg("dim", "·")} ${presentation.label}`), 0, 0));
1516
1713
  c.addChild(new Spacer(1));
1517
1714
  const taskMaxLen = Math.max(20, w - 8);
1518
1715
  const taskPreview = expanded || r.task.length <= taskMaxLen
@@ -1593,31 +1790,30 @@ export function renderSubagentResult(
1593
1790
 
1594
1791
  if (!expanded) return renderMultiCompact(d, theme, frame);
1595
1792
 
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"
1793
+ const hasRunning = detailsHaveRunningResult(d);
1794
+ const detached = d.results.some((r) => r.detached)
1795
+ || workflowGraphHasStatus(d, ["detached"]);
1796
+ const stopped = d.results.some((r) => r.stopped)
1797
+ || workflowGraphHasStatus(d, ["stopped"]);
1798
+ const failed = d.results.some((r) => !hasTerminalResultFlag(r) && r.exitCode !== 0 && !isResultRunning(r))
1799
+ || workflowGraphHasStatus(d, ["failed"]);
1800
+ const paused = d.results.some((r) => r.interrupted)
1801
+ || workflowGraphHasStatus(d, ["paused"]);
1802
+ const completedWithoutOutput = d.results.some((r) =>
1803
+ !hasTerminalResultFlag(r)
1804
+ && r.exitCode === 0
1805
+ && !isResultRunning(r)
1603
1806
  && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1604
1807
  );
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");
1808
+ const presentation = styledResultPresentation(semanticResultPresentation({
1809
+ running: hasRunning,
1810
+ detached,
1811
+ stopped,
1812
+ interrupted: paused,
1813
+ failed,
1814
+ completedWithoutOutput,
1815
+ frame,
1816
+ }), theme);
1621
1817
 
1622
1818
  const totalSummary =
1623
1819
  d.progressSummary ||
@@ -1654,22 +1850,14 @@ export function renderSubagentResult(
1654
1850
  ? d.chainAgents
1655
1851
  .map((agent, i) => {
1656
1852
  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
1853
  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)}`;
1854
+ const stepPresentation = result
1855
+ ? styledResultPresentation(resultPresentation(result, getSingleResultOutput(result), isCurrent && hasRunning && !hasTerminalResultFlag(result)), theme)
1856
+ : undefined;
1857
+ const stepStatus = stepPresentation
1858
+ ? `${stepPresentation.glyph} ${stepPresentation.label}`
1859
+ : theme.fg("dim", "◦ pending");
1860
+ return `${stepStatus} ${agent}${contextModeBadge(theme, result?.context)}`;
1673
1861
  })
1674
1862
  .join(theme.fg("dim", " → "))
1675
1863
  : null;
@@ -1679,7 +1867,7 @@ export function renderSubagentResult(
1679
1867
  const c = new Container();
1680
1868
  c.addChild(
1681
1869
  new Text(
1682
- fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr}`),
1870
+ fit(`${presentation.glyph} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr} ${theme.fg("dim", "·")} ${presentation.label}`),
1683
1871
  0,
1684
1872
  0,
1685
1873
  ),
@@ -1726,24 +1914,18 @@ export function renderSubagentResult(
1726
1914
  const progressFromArray = d.progress?.find((p) => p.index === i)
1727
1915
  || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1728
1916
  const rProg = r.progress || progressFromArray || r.progressSummary;
1729
- const rRunning = rProg?.status === "running";
1917
+ const rRunning = isResultRunning(r, rProg?.status);
1730
1918
  const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
1731
1919
 
1732
1920
  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");
1921
+ const rowPresentation = styledResultPresentation(resultPresentation(r, resultOutput, rRunning, progressRunningSeed(rProg), frame), theme);
1740
1922
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
1741
- const modelDisplay = modelThinkingBadge(theme, r.model);
1923
+ const modelDisplay = modelThinkingBadge(theme, r.model ?? rProg?.model, r.thinking ?? rProg?.thinking);
1742
1924
  const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
1743
1925
  const contextBadge = contextModeBadge(theme, r.context);
1744
1926
  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}`;
1927
+ ? `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`
1928
+ : `${rowPresentation.glyph} ${stepLabel}: ${theme.bold(r.agent)}${contextBadge}${modelDisplay}${stats} ${theme.fg("dim", "·")} ${rowPresentation.label}`;
1747
1929
  const toolCallLines = getToolCallLines(r, expanded);
1748
1930
  c.addChild(new Text(fit(stepHeader), 0, 0));
1749
1931
 
@@ -0,0 +1,14 @@
1
+ declare module "@earendil-works/pi-agent-core" {
2
+ interface AgentToolResult<T> {
3
+ /** Runtime error flag emitted and rendered by pi tool execution. */
4
+ isError?: boolean;
5
+ }
6
+ }
7
+
8
+ declare module "@earendil-works/pi-coding-agent" {
9
+ interface ExtensionUIContext {
10
+ requestRender?: () => void;
11
+ }
12
+ }
13
+
14
+ export {};
@@ -260,6 +260,7 @@ class JsonRpcLspClient {
260
260
  private readonly child: ChildProcessWithoutNullStreams;
261
261
  private stderr = "";
262
262
  private exited = false;
263
+ private terminating = false;
263
264
  private readonly exitWaiters: Array<() => void> = [];
264
265
 
265
266
  constructor(child: ChildProcessWithoutNullStreams) {
@@ -295,17 +296,21 @@ class JsonRpcLspClient {
295
296
 
296
297
  async shutdown(): Promise<void> {
297
298
  if (this.exited) return;
298
- try {
299
- await this.request("shutdown", null, SHUTDOWN_TIMEOUT_MS);
300
- this.notify("exit", null);
301
- } catch {
302
- this.child.kill("SIGTERM");
299
+ if (!this.terminating) {
300
+ try {
301
+ await this.request("shutdown", null, SHUTDOWN_TIMEOUT_MS);
302
+ this.notify("exit", null);
303
+ } catch {
304
+ this.kill();
305
+ }
303
306
  }
304
307
  await this.waitForExit(SHUTDOWN_TIMEOUT_MS);
305
308
  }
306
309
 
307
310
  kill(): void {
308
- if (!this.exited) this.child.kill("SIGTERM");
311
+ if (this.exited || this.terminating) return;
312
+ this.terminating = true;
313
+ this.child.kill("SIGTERM");
309
314
  }
310
315
 
311
316
  stderrTail(): string {
@@ -367,7 +372,7 @@ class JsonRpcLspClient {
367
372
  private failProtocol(error: Error): void {
368
373
  if (this.exited) return;
369
374
  this.rejectPending(error);
370
- this.child.kill("SIGTERM");
375
+ this.kill();
371
376
  }
372
377
 
373
378
  private waitForExit(timeoutMs: number): Promise<void> {