pi-subagents 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. package/src/runs/shared/turn-budget.ts +0 -98
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import type { Message } from "@earendil-works/pi-ai";
3
3
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
4
4
  import { previewSimpleWorkflowRun } from "../workflows/scripted-workflow.ts";
5
+ import { deriveChildSessionName } from "../shared/child-session-name.ts";
5
6
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
6
7
  import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "../shared/types.ts";
7
8
 
@@ -54,9 +55,11 @@ function createPlaceholderResult(
54
55
  status: "pending" | "running",
55
56
  index: number,
56
57
  ): SingleResult {
58
+ const sessionName = deriveChildSessionName({ agent, task });
57
59
  return {
58
60
  agent,
59
61
  task,
62
+ ...(sessionName ? { sessionName } : {}),
60
63
  index,
61
64
  exitCode: 0,
62
65
  messages: EMPTY_MESSAGES,
@@ -64,6 +67,7 @@ function createPlaceholderResult(
64
67
  progress: {
65
68
  index,
66
69
  agent,
70
+ ...(sessionName ? { sessionName } : {}),
67
71
  status,
68
72
  task,
69
73
  recentTools: [],
@@ -84,17 +88,21 @@ function buildParallelInitialResult(params: SubagentParamsLike): AgentToolResult
84
88
  ...(params.async ? { background: true } : {}),
85
89
  ...(params.context === "fresh" || params.context === "fork" ? { context: params.context } : {}),
86
90
  results: tasks.map((task, index) => createPlaceholderResult(task.agent, task.task, "running", index)),
87
- progress: tasks.map((task, index) => ({
88
- index,
89
- agent: task.agent,
90
- status: "running" as const,
91
- task: task.task,
92
- recentTools: [],
93
- recentOutput: [],
94
- toolCount: 0,
95
- tokens: 0,
96
- durationMs: 0,
97
- })),
91
+ progress: tasks.map((task, index) => {
92
+ const sessionName = deriveChildSessionName({ agent: task.agent, task: task.task });
93
+ return {
94
+ index,
95
+ agent: task.agent,
96
+ ...(sessionName ? { sessionName } : {}),
97
+ status: "running" as const,
98
+ task: task.task,
99
+ recentTools: [],
100
+ recentOutput: [],
101
+ toolCount: 0,
102
+ tokens: 0,
103
+ durationMs: 0,
104
+ };
105
+ }),
98
106
  },
99
107
  };
100
108
  }
@@ -143,6 +151,7 @@ function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<De
143
151
  progress: results.map((result, index) => ({
144
152
  index,
145
153
  agent: result.agent,
154
+ ...(result.sessionName ? { sessionName: result.sessionName } : {}),
146
155
  status: index === 0 ? "running" as const : "pending" as const,
147
156
  task: result.task,
148
157
  recentTools: [],
@@ -162,6 +171,7 @@ function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<D
162
171
  const preview = previewSimpleWorkflowRun(params.workflowScript) ?? {};
163
172
  const agent = params.agent ?? preview.agent ?? "subagent";
164
173
  const task = params.task ?? preview.task ?? "";
174
+ const sessionName = deriveChildSessionName({ agent, task });
165
175
  return {
166
176
  content: [{ type: "text", text: task }],
167
177
  details: {
@@ -172,6 +182,7 @@ function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<D
172
182
  progress: [{
173
183
  index: 0,
174
184
  agent,
185
+ ...(sessionName ? { sessionName } : {}),
175
186
  status: "running",
176
187
  task,
177
188
  recentTools: [],
@@ -89,6 +89,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
89
89
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
90
90
  systemPromptMode: agent.systemPromptMode,
91
91
  inheritProjectContext: agent.inheritProjectContext,
92
+ inheritGlobalContext: agent.inheritGlobalContext,
92
93
  inheritSkills: agent.inheritSkills,
93
94
  ...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
94
95
  ...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
@@ -98,6 +99,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
98
99
  ...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
99
100
  ...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
100
101
  ...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
102
+ ...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
101
103
  ...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
102
104
  ...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
103
105
  };
@@ -189,6 +191,7 @@ function metadataFor(agent: AgentConfig): string {
189
191
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
190
192
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
191
193
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
194
+ lines.push(`Inherit global context: ${agent.inheritGlobalContext ? "true" : "false"}`);
192
195
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
193
196
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
194
197
  if (agent.output) lines.push(`Output: ${agent.output}`);
@@ -2,8 +2,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type EditorComponent, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { snapshotExternalRuns } from "../api/external-runs.ts";
4
4
  import { formatModelThinking } from "../shared/formatters.ts";
5
- import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
5
+ import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
6
+ import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
6
7
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
8
+ import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
9
+ import { isStaleExtensionContextError } from "../shared/extension-context.ts";
7
10
 
8
11
  export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
9
12
 
@@ -30,18 +33,7 @@ type FleetStatusEntry = {
30
33
  external?: true;
31
34
  projectPane?: HerdrProjectPaneSnapshot;
32
35
  nestedChildren?: NestedRunSummary[];
33
- workflowRows?: FleetWorkflowRow[];
34
- };
35
-
36
- type FleetWorkflowRow = {
37
- name: string;
38
- state: AsyncJobStep["status"];
39
- modelThinking?: string;
40
- activity?: string;
41
- startedAt?: number;
42
- tokens?: number;
43
- window?: number;
44
- overflow?: number;
36
+ workflowRows?: AsyncStatusWorkflowRow[];
45
37
  };
46
38
 
47
39
  type FleetNestedRow = {
@@ -57,7 +49,7 @@ type FleetNestedRow = {
57
49
  type FleetTreeRow =
58
50
  | { kind: "owner"; entry: FleetStatusEntry }
59
51
  | { kind: "child"; entry: FleetStatusEntry; last: boolean }
60
- | { kind: "workflow"; ownerKey: string; row: FleetWorkflowRow; last: boolean }
52
+ | { kind: "workflow"; ownerKey: string; row: AsyncStatusWorkflowRow; last: boolean }
61
53
  | { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
62
54
 
63
55
  export interface FleetStatusOptions {
@@ -111,45 +103,12 @@ function nestedActivity(node: NestedRunSummary | NestedStepSummary): string | un
111
103
  return undefined;
112
104
  }
113
105
 
114
- function workflowStepActivity(step: AsyncJobStep): string | undefined {
115
- if (step.currentTool) return `tool ${step.currentTool}`;
116
- if (step.currentPath) return step.currentPath.split(/[\\/]/).at(-1);
117
- if (step.activityState === "needs_attention") return "needs attention";
118
- if (step.activityState === "active_long_running") return "long-running";
119
- if (step.turnCount !== undefined) return `${step.turnCount} turns`;
120
- if (step.toolCount !== undefined) return `${step.toolCount} tools`;
121
- return undefined;
122
- }
123
-
124
- function workflowStepName(step: AsyncJobStep, index: number): string {
125
- const key = step.workflowKey ?? `step ${index + 1}`;
126
- const label = step.label && step.label !== key ? ` · ${step.label}` : "";
127
- const phase = step.phase ? `${step.phase}: ` : "";
128
- return `${phase}${key}${label} (${step.agent})`;
129
- }
130
-
131
- function workflowFleetRows(steps: AsyncJobStep[] | undefined): FleetWorkflowRow[] {
132
- return (steps ?? []).map((step, index) => {
133
- const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
134
- const activity = workflowStepActivity(step);
135
- return {
136
- name: workflowStepName(step, index),
137
- state: step.status,
138
- ...(modelThinking ? { modelThinking } : {}),
139
- ...(activity ? { activity } : {}),
140
- ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
141
- ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
142
- ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
143
- };
144
- });
145
- }
146
-
147
- function visibleWorkflowRows(rows: FleetWorkflowRow[] | undefined, visibleLimit: number): FleetWorkflowRow[] {
106
+ function visibleWorkflowRows(rows: AsyncStatusWorkflowRow[] | undefined, visibleLimit: number): AsyncStatusWorkflowRow[] {
148
107
  if (!rows?.length) return [];
149
108
  if (rows.length <= visibleLimit) return rows;
150
109
  const selected = new Set<number>();
151
110
  for (const [index, row] of rows.entries()) {
152
- if (row.state !== "complete" && row.state !== "completed") selected.add(index);
111
+ if (!isWorkflowRowTerminal(row)) selected.add(index);
153
112
  if (selected.size >= visibleLimit) break;
154
113
  }
155
114
  for (let index = rows.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
@@ -157,9 +116,14 @@ function visibleWorkflowRows(rows: FleetWorkflowRow[] | undefined, visibleLimit:
157
116
  return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
158
117
  }
159
118
 
160
- function nestedStatusGlyph(state: FleetNestedRow["state"], theme: Theme): string {
119
+ function isWorkflowRowTerminal(row: AsyncStatusWorkflowRow): boolean {
120
+ if (row.kind) return row.state === "done" || row.state === "cancelled" || row.state === "error";
121
+ return row.state === "complete" || row.state === "completed";
122
+ }
123
+
124
+ function nestedStatusGlyph(state: FleetNestedRow["state"] | "planned", theme: Theme): string {
161
125
  if (state === "running") return theme.fg("accent", "●");
162
- if (state === "queued" || state === "pending") return theme.fg("muted", "◦");
126
+ if (state === "queued" || state === "pending" || state === "planned") return theme.fg("muted", "◦");
163
127
  if (state === "complete" || state === "completed") return theme.fg("success", "✓");
164
128
  if (state === "failed" || state === "rejected") return theme.fg("error", "✗");
165
129
  return theme.fg("warning", "■");
@@ -278,19 +242,31 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
278
242
  return rows;
279
243
  }
280
244
 
281
- function isStaleExtensionContextError(error: unknown): boolean {
282
- // Pi currently exposes stale contexts as plain Errors without a stable code or subtype.
283
- return error instanceof Error
284
- && (error.message.includes("This extension ctx is stale")
285
- || error.message.includes("Extension context no longer active"));
286
- }
287
-
288
245
  function foregroundDescription(control: { parentWorkflowRunId?: string; workflowKey?: string }, description: string | undefined): string | undefined {
289
246
  if (!control.parentWorkflowRunId) return description;
290
247
  const workflow = `workflow child: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}`;
291
248
  return description ? `${workflow} · ${description}` : workflow;
292
249
  }
293
250
 
251
+ function workflowIdentityCandidates(step: Pick<AsyncJobStep, "workflowKey" | "runId">): string[] {
252
+ return [...new Set([step.workflowKey, step.runId].filter((value): value is string => typeof value === "string" && value.length > 0))];
253
+ }
254
+
255
+ function asyncJobIdentityCandidates(job: AsyncJobState): string[] {
256
+ return [...new Set([job.workflowKey, job.asyncId].filter((value): value is string => typeof value === "string" && value.length > 0))];
257
+ }
258
+
259
+ function linkedWorkflowParentKey(parentWorkflowRunId: string | undefined, activeWorkflowKeys: ReadonlySet<string>): string | undefined {
260
+ if (!parentWorkflowRunId) return undefined;
261
+ const parentKey = `async:${parentWorkflowRunId}`;
262
+ return activeWorkflowKeys.has(parentKey) ? parentKey : undefined;
263
+ }
264
+
265
+ function workflowStepsWithoutMaterializedChildren(steps: AsyncJobStep[] | undefined, materializedChildIds: ReadonlySet<string> | undefined): AsyncJobStep[] | undefined {
266
+ if (!steps?.length || !materializedChildIds?.size) return steps;
267
+ return steps.filter((step) => !workflowIdentityCandidates(step).some((identity) => materializedChildIds.has(identity)));
268
+ }
269
+
294
270
  function activeLeafAgentCount(entries: FleetStatusEntry[]): number {
295
271
  return entries.filter((entry) => !entry.workflowWrapper && !entry.surface).length;
296
272
  }
@@ -325,12 +301,30 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
325
301
  const activeWorkflowKeys = new Set([...state.asyncJobs.values()]
326
302
  .filter((job) => job.mode === "workflow" && isActiveState(job.status))
327
303
  .map((job) => `async:${job.asyncId}`));
304
+ const materializedChildrenByWorkflow = new Map<string, Set<string>>();
305
+ for (const job of state.asyncJobs.values()) {
306
+ if (!isActiveState(job.status) || !job.parentWorkflowRunId) continue;
307
+ const parentKey = linkedWorkflowParentKey(job.parentWorkflowRunId, activeWorkflowKeys);
308
+ if (!parentKey) continue;
309
+ const childIds = materializedChildrenByWorkflow.get(parentKey) ?? new Set<string>();
310
+ for (const identity of asyncJobIdentityCandidates(job)) childIds.add(identity);
311
+ materializedChildrenByWorkflow.set(parentKey, childIds);
312
+ }
328
313
  for (const control of state.foregroundControls.values()) {
329
- const parentKey = control.parentWorkflowRunId ? `async:${control.parentWorkflowRunId}` : undefined;
330
- const linkedParentKey = parentKey && activeWorkflowKeys.has(parentKey) ? parentKey : undefined;
314
+ const linkedParentKey = linkedWorkflowParentKey(control.parentWorkflowRunId, activeWorkflowKeys);
331
315
  if (control.activeChildren) {
316
+ const nestedChildren = control.nestedChildren ?? [];
317
+ const nestedChildrenByParentStep = new Map<number, NestedRunSummary[]>();
318
+ for (const nested of nestedChildren) {
319
+ if (nested.parentStepIndex === undefined) continue;
320
+ const children = nestedChildrenByParentStep.get(nested.parentStepIndex) ?? [];
321
+ children.push(nested);
322
+ nestedChildrenByParentStep.set(nested.parentStepIndex, children);
323
+ }
332
324
  for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
333
325
  const modelThinking = formatModelThinking(child.model, child.thinking) || undefined;
326
+ const childNestedChildren = nestedChildrenByParentStep.get(child.index)
327
+ ?? (control.activeChildren.size === 1 && nestedChildren.length ? nestedChildren : undefined);
334
328
  entries.push({
335
329
  key: `foreground-active:${control.runId}:${child.index}`,
336
330
  ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
@@ -338,12 +332,10 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
338
332
  ...(modelThinking ? { modelThinking } : {}),
339
333
  description: foregroundDescription(control, child.description),
340
334
  startedAt: child.startedAt,
341
- tokens: child.tokens ?? 0,
342
- ...(child.window !== undefined ? { window: child.window } : {}),
335
+ tokens: child.tokens ?? 0,
336
+ ...(child.window !== undefined ? { window: child.window } : {}),
343
337
  state: "running",
344
- ...((control.nestedChildren?.filter((nested) => nested.parentStepIndex === child.index).length ?? 0) > 0
345
- ? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
346
- : control.activeChildren.size === 1 && control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
338
+ ...(childNestedChildren?.length ? { nestedChildren: childNestedChildren } : {}),
347
339
  });
348
340
  }
349
341
  continue;
@@ -366,10 +358,14 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
366
358
  for (const job of state.asyncJobs.values()) {
367
359
  if (!isActiveState(job.status)) continue;
368
360
  const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
361
+ const linkedParentKey = linkedWorkflowParentKey(job.parentWorkflowRunId, activeWorkflowKeys);
369
362
  if (job.mode === "workflow") {
370
363
  const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
364
+ const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
365
+ const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.hostSteps, job.preflight);
371
366
  entries.push({
372
367
  key: `async:${job.asyncId}`,
368
+ ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
373
369
  workflowWrapper: true,
374
370
  agent: "workflow",
375
371
  description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
@@ -377,7 +373,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
377
373
  tokens: job.totalTokens?.total ?? 0,
378
374
  ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
379
375
  state: job.status,
380
- ...(job.steps?.length ? { workflowRows: workflowFleetRows(job.steps) } : {}),
376
+ ...(workflowRows.length ? { workflowRows } : {}),
381
377
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
382
378
  });
383
379
  continue;
@@ -392,6 +388,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
392
388
  if (!steps?.length) {
393
389
  entries.push({
394
390
  key: `async:${job.asyncId}`,
391
+ ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
395
392
  agent: job.mode ?? "subagent",
396
393
  description: job.description,
397
394
  startedAt,
@@ -409,6 +406,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
409
406
  const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
410
407
  entries.push({
411
408
  key: `async:${job.asyncId}:${index}`,
409
+ ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
412
410
  agent: step.label ? `${step.label} (${step.agent})` : step.agent,
413
411
  ...(modelThinking ? { modelThinking } : {}),
414
412
  description: step.description ?? job.description,
@@ -643,6 +641,10 @@ export class SubagentFleetStatus {
643
641
  }
644
642
  const roster = this.rosterKeys();
645
643
  const selectedIndex = Math.max(0, roster.indexOf(this.selectedKey));
644
+ const rosterIndexByKey = new Map<string, number>();
645
+ for (const [index, entry] of this.entries.entries()) {
646
+ if (!rosterIndexByKey.has(entry.key)) rosterIndexByKey.set(entry.key, index + 1);
647
+ }
646
648
  const lines = [truncateToWidth(` ${theme.fg("dim", "↑↓/jk select · enter inspect · esc back")}`, width), ""];
647
649
  lines.push(truncateToWidth(` ${this.bullet(0, selectedIndex, theme)} main`, width));
648
650
 
@@ -656,8 +658,8 @@ export class SubagentFleetStatus {
656
658
  for (let index = start; index < start + visibleCount; index++) {
657
659
  const row = tree[index]!;
658
660
  if (row.kind === "owner" || row.kind === "child") {
659
- const ownerIndex = this.entries.findIndex((entry) => entry.key === row.entry.key);
660
- lines.push(this.renderEntry(ownerIndex + 1, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
661
+ const rosterIndex = rosterIndexByKey.get(row.entry.key) ?? 0;
662
+ lines.push(this.renderEntry(rosterIndex, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
661
663
  } else if (row.kind === "workflow") {
662
664
  lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
663
665
  } else {
@@ -665,16 +667,16 @@ export class SubagentFleetStatus {
665
667
  }
666
668
  }
667
669
  if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
668
- this.renderProjectPaneSection(lines, selectedIndex, width, theme);
670
+ this.renderProjectPaneSection(lines, selectedIndex, width, theme, rosterIndexByKey);
669
671
  return lines;
670
672
  }
671
673
 
672
- private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme): void {
674
+ private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme, rosterIndexByKey: ReadonlyMap<string, number>): void {
673
675
  const entries = this.entries.filter((entry) => entry.surface === "project-pane");
674
676
  if (!entries.length) return;
675
677
  lines.push("", truncateToWidth(` ${theme.fg("dim", "project panes")}`, width));
676
678
  for (const entry of entries) {
677
- const rosterIndex = this.entries.findIndex((candidate) => candidate.key === entry.key) + 1;
679
+ const rosterIndex = rosterIndexByKey.get(entry.key) ?? 0;
678
680
  lines.push(this.renderEntry(rosterIndex, selectedIndex, entry, width, theme));
679
681
  }
680
682
  }
@@ -703,16 +705,50 @@ export class SubagentFleetStatus {
703
705
  return truncateToWidth(`${left}${theme.fg("dim", elapsed)}`, width);
704
706
  }
705
707
 
706
- private renderWorkflowRow(row: FleetWorkflowRow, last: boolean, width: number, theme: Theme): string {
708
+ private workflowRowGlyph(row: AsyncStatusWorkflowRow, theme: Theme): string {
709
+ if (!row.kind) return nestedStatusGlyph(row.state as FleetNestedRow["state"], theme);
710
+ const state = row.state as HostStepState;
711
+ if (state === "pending") return theme.fg("muted", "◦");
712
+ if (state === "running") return theme.fg("accent", "●");
713
+ if (state === "done") return row.verdict === "pass" ? theme.fg("success", "✓") : row.verdict === "fail" ? theme.fg("error", "✗") : theme.fg("warning", "■");
714
+ if (state === "error") return theme.fg("error", "✗");
715
+ return theme.fg("warning", "■");
716
+ }
717
+
718
+ private workflowRowStateLabel(row: AsyncStatusWorkflowRow, theme: Theme): string {
719
+ const state = row.kind ? hostStepVerdictLabel(row.state as HostStepState, row.verdict as HostStepVerdict | undefined) : row.state;
720
+ if (state === "running") return theme.fg("accent", state);
721
+ if (state === "pending" || state === "queued") return theme.fg("muted", state);
722
+ if (state === "pass" || state === "complete" || state === "completed") return theme.fg("success", state === "pass" ? "pass" : "complete");
723
+ if (state === "fail" || state === "failed" || state === "error") return theme.fg("error", state === "fail" ? "fail" : state);
724
+ return theme.fg("warning", state);
725
+ }
726
+
727
+ private renderWorkflowRow(row: AsyncStatusWorkflowRow, last: boolean, width: number, theme: Theme): string {
707
728
  const marker = last ? "└─" : "├─";
708
729
  const indent = " ";
709
730
  if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} hidden workflow steps`)}`, width);
710
731
  const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
711
732
  const activity = row.activity ? ` · ${row.activity}` : "";
712
- const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
733
+ const kind = row.kind ? `${row.kind}: ` : "";
734
+ const hints = row.preflight ? [
735
+ row.preflight.mode ? `mode:${row.preflight.mode}` : undefined,
736
+ row.preflight.decision ? `decision:${row.preflight.decision}` : undefined,
737
+ row.preflight.claims?.length ? `claims:${row.preflight.claims.join(",")}` : undefined,
738
+ row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
739
+ row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
740
+ ].filter((value): value is string => Boolean(value)).join(" · ") : "";
741
+ const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
713
742
  const details = [
714
743
  row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
715
744
  row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
745
+ row.provider ? `provider:${row.provider}` : undefined,
746
+ row.role ? `role:${row.role}` : undefined,
747
+ row.target,
748
+ row.detail,
749
+ row.reasonCode ? `reason:${row.reasonCode}` : undefined,
750
+ row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : undefined,
751
+ row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : undefined,
716
752
  ].filter(Boolean).join(" · ");
717
753
  return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
718
754
  }
@@ -767,12 +803,21 @@ export class SubagentFleetStatus {
767
803
  Math.round((now - entry.startedAt) / 1000),
768
804
  entry.tokens,
769
805
  visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
806
+ row.kind,
770
807
  row.name,
771
808
  row.state,
772
809
  row.modelThinking,
773
810
  row.activity,
774
811
  row.startedAt,
775
812
  row.tokens,
813
+ row.provider,
814
+ row.role,
815
+ row.verdict,
816
+ row.reasonCode,
817
+ row.detail,
818
+ row.target,
819
+ row.freshness,
820
+ row.reportPath,
776
821
  row.overflow,
777
822
  ]),
778
823
  nestedFleetRows(entry.nestedChildren, entry.parentKey ? 3 : 4).map((row) => [
@@ -807,7 +852,13 @@ export class SubagentFleetStatus {
807
852
 
808
853
  private clearWidget(): void {
809
854
  if (!this.widgetRegistered) return;
810
- this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
855
+ try {
856
+ this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
857
+ } catch (error) {
858
+ if (!isStaleExtensionContextError(error)) throw error;
859
+ this.clearUiRegistration();
860
+ return;
861
+ }
811
862
  this.widgetRegistered = false;
812
863
  this.tui = undefined;
813
864
  }
package/src/tui/fleet.ts CHANGED
@@ -147,6 +147,7 @@ function trackedJobSummary(job: AsyncJobState): AsyncRunSummary {
147
147
  ...(job.currentStep !== undefined ? { currentStep: job.currentStep } : {}),
148
148
  ...(job.chainStepCount !== undefined ? { chainStepCount: job.chainStepCount } : {}),
149
149
  ...(job.parallelGroups?.length ? { parallelGroups: job.parallelGroups } : {}),
150
+ ...(job.preflight ? { preflight: job.preflight } : {}),
150
151
  steps: (job.steps ?? job.agents?.map((agent, index) => ({ agent, index, status: job.status === "queued" ? "pending" as const : job.status })) ?? []).map((step, index) => ({
151
152
  ...step,
152
153
  index: step.index ?? index,
@@ -355,7 +356,12 @@ function foregroundPromptAuditCount(item: Extract<FleetItem, { kind: "foreground
355
356
  return [...item.control.activeChildren.keys()].filter((index) => getLivePromptAudit(item.control, index)).length;
356
357
  }
357
358
 
358
- function authoredPromptSummary(text: string): string | undefined {
359
+ function promptAuditString(value: unknown): string | undefined {
360
+ return typeof value === "string" ? value : undefined;
361
+ }
362
+
363
+ function authoredPromptSummary(text: unknown): string | undefined {
364
+ if (typeof text !== "string") return undefined;
359
365
  const summary = text.replace(/\s+/g, " ").trim();
360
366
  return summary ? truncateToWidth(summary, PROMPT_AUDIT_SUMMARY_WIDTH, "…") : undefined;
361
367
  }
@@ -366,11 +372,11 @@ function foregroundAuthoredPromptSummary(item: Extract<FleetItem, { kind: "foreg
366
372
  return prompt ? authoredPromptSummary(prompt.authoredTask) : undefined;
367
373
  }
368
374
 
369
- function promptAuditText(prompt: LivePromptAudit, view: PromptAuditView): string {
375
+ function promptAuditText(prompt: LivePromptAudit, view: PromptAuditView): string | undefined {
370
376
  switch (view) {
371
- case "authored": return prompt.authoredTask;
372
- case "runtime": return prompt.runtimeAdditions;
373
- case "effective": return prompt.finalEffectivePrompt;
377
+ case "authored": return promptAuditString(prompt.authoredTask);
378
+ case "runtime": return promptAuditString(prompt.runtimeAdditions);
379
+ case "effective": return promptAuditString(prompt.finalEffectivePrompt);
374
380
  }
375
381
  }
376
382