pi-subagents 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- 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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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: [],
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -2,8 +2,12 @@ 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";
|
|
7
|
+
import { contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
6
8
|
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
9
|
+
import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
|
|
10
|
+
import { isStaleExtensionContextError } from "../shared/extension-context.ts";
|
|
7
11
|
|
|
8
12
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
9
13
|
|
|
@@ -30,18 +34,7 @@ type FleetStatusEntry = {
|
|
|
30
34
|
external?: true;
|
|
31
35
|
projectPane?: HerdrProjectPaneSnapshot;
|
|
32
36
|
nestedChildren?: NestedRunSummary[];
|
|
33
|
-
workflowRows?:
|
|
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;
|
|
37
|
+
workflowRows?: AsyncStatusWorkflowRow[];
|
|
45
38
|
};
|
|
46
39
|
|
|
47
40
|
type FleetNestedRow = {
|
|
@@ -57,7 +50,7 @@ type FleetNestedRow = {
|
|
|
57
50
|
type FleetTreeRow =
|
|
58
51
|
| { kind: "owner"; entry: FleetStatusEntry }
|
|
59
52
|
| { kind: "child"; entry: FleetStatusEntry; last: boolean }
|
|
60
|
-
| { kind: "workflow"; ownerKey: string; row:
|
|
53
|
+
| { kind: "workflow"; ownerKey: string; row: AsyncStatusWorkflowRow; last: boolean }
|
|
61
54
|
| { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
|
|
62
55
|
|
|
63
56
|
export interface FleetStatusOptions {
|
|
@@ -111,45 +104,12 @@ function nestedActivity(node: NestedRunSummary | NestedStepSummary): string | un
|
|
|
111
104
|
return undefined;
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
function
|
|
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[] {
|
|
107
|
+
function visibleWorkflowRows(rows: AsyncStatusWorkflowRow[] | undefined, visibleLimit: number): AsyncStatusWorkflowRow[] {
|
|
148
108
|
if (!rows?.length) return [];
|
|
149
109
|
if (rows.length <= visibleLimit) return rows;
|
|
150
110
|
const selected = new Set<number>();
|
|
151
111
|
for (const [index, row] of rows.entries()) {
|
|
152
|
-
if (row
|
|
112
|
+
if (!isWorkflowRowTerminal(row)) selected.add(index);
|
|
153
113
|
if (selected.size >= visibleLimit) break;
|
|
154
114
|
}
|
|
155
115
|
for (let index = rows.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
|
|
@@ -157,9 +117,14 @@ function visibleWorkflowRows(rows: FleetWorkflowRow[] | undefined, visibleLimit:
|
|
|
157
117
|
return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
|
|
158
118
|
}
|
|
159
119
|
|
|
160
|
-
function
|
|
120
|
+
function isWorkflowRowTerminal(row: AsyncStatusWorkflowRow): boolean {
|
|
121
|
+
if (row.kind) return row.state === "done" || row.state === "cancelled" || row.state === "error";
|
|
122
|
+
return row.state === "complete" || row.state === "completed";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function nestedStatusGlyph(state: FleetNestedRow["state"] | "planned", theme: Theme): string {
|
|
161
126
|
if (state === "running") return theme.fg("accent", "●");
|
|
162
|
-
if (state === "queued" || state === "pending") return theme.fg("muted", "◦");
|
|
127
|
+
if (state === "queued" || state === "pending" || state === "planned") return theme.fg("muted", "◦");
|
|
163
128
|
if (state === "complete" || state === "completed") return theme.fg("success", "✓");
|
|
164
129
|
if (state === "failed" || state === "rejected") return theme.fg("error", "✗");
|
|
165
130
|
return theme.fg("warning", "■");
|
|
@@ -278,19 +243,31 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
278
243
|
return rows;
|
|
279
244
|
}
|
|
280
245
|
|
|
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
246
|
function foregroundDescription(control: { parentWorkflowRunId?: string; workflowKey?: string }, description: string | undefined): string | undefined {
|
|
289
247
|
if (!control.parentWorkflowRunId) return description;
|
|
290
248
|
const workflow = `workflow child: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}`;
|
|
291
249
|
return description ? `${workflow} · ${description}` : workflow;
|
|
292
250
|
}
|
|
293
251
|
|
|
252
|
+
function workflowIdentityCandidates(step: Pick<AsyncJobStep, "workflowKey" | "runId">): string[] {
|
|
253
|
+
return [...new Set([step.workflowKey, step.runId].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function asyncJobIdentityCandidates(job: AsyncJobState): string[] {
|
|
257
|
+
return [...new Set([job.workflowKey, job.asyncId].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function linkedWorkflowParentKey(parentWorkflowRunId: string | undefined, activeWorkflowKeys: ReadonlySet<string>): string | undefined {
|
|
261
|
+
if (!parentWorkflowRunId) return undefined;
|
|
262
|
+
const parentKey = `async:${parentWorkflowRunId}`;
|
|
263
|
+
return activeWorkflowKeys.has(parentKey) ? parentKey : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function workflowStepsWithoutMaterializedChildren(steps: AsyncJobStep[] | undefined, materializedChildIds: ReadonlySet<string> | undefined): AsyncJobStep[] | undefined {
|
|
267
|
+
if (!steps?.length || !materializedChildIds?.size) return steps;
|
|
268
|
+
return steps.filter((step) => !workflowIdentityCandidates(step).some((identity) => materializedChildIds.has(identity)));
|
|
269
|
+
}
|
|
270
|
+
|
|
294
271
|
function activeLeafAgentCount(entries: FleetStatusEntry[]): number {
|
|
295
272
|
return entries.filter((entry) => !entry.workflowWrapper && !entry.surface).length;
|
|
296
273
|
}
|
|
@@ -325,12 +302,30 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
325
302
|
const activeWorkflowKeys = new Set([...state.asyncJobs.values()]
|
|
326
303
|
.filter((job) => job.mode === "workflow" && isActiveState(job.status))
|
|
327
304
|
.map((job) => `async:${job.asyncId}`));
|
|
305
|
+
const materializedChildrenByWorkflow = new Map<string, Set<string>>();
|
|
306
|
+
for (const job of state.asyncJobs.values()) {
|
|
307
|
+
if (!isActiveState(job.status) || !job.parentWorkflowRunId) continue;
|
|
308
|
+
const parentKey = linkedWorkflowParentKey(job.parentWorkflowRunId, activeWorkflowKeys);
|
|
309
|
+
if (!parentKey) continue;
|
|
310
|
+
const childIds = materializedChildrenByWorkflow.get(parentKey) ?? new Set<string>();
|
|
311
|
+
for (const identity of asyncJobIdentityCandidates(job)) childIds.add(identity);
|
|
312
|
+
materializedChildrenByWorkflow.set(parentKey, childIds);
|
|
313
|
+
}
|
|
328
314
|
for (const control of state.foregroundControls.values()) {
|
|
329
|
-
const
|
|
330
|
-
const linkedParentKey = parentKey && activeWorkflowKeys.has(parentKey) ? parentKey : undefined;
|
|
315
|
+
const linkedParentKey = linkedWorkflowParentKey(control.parentWorkflowRunId, activeWorkflowKeys);
|
|
331
316
|
if (control.activeChildren) {
|
|
317
|
+
const nestedChildren = control.nestedChildren ?? [];
|
|
318
|
+
const nestedChildrenByParentStep = new Map<number, NestedRunSummary[]>();
|
|
319
|
+
for (const nested of nestedChildren) {
|
|
320
|
+
if (nested.parentStepIndex === undefined) continue;
|
|
321
|
+
const children = nestedChildrenByParentStep.get(nested.parentStepIndex) ?? [];
|
|
322
|
+
children.push(nested);
|
|
323
|
+
nestedChildrenByParentStep.set(nested.parentStepIndex, children);
|
|
324
|
+
}
|
|
332
325
|
for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
|
|
333
326
|
const modelThinking = formatModelThinking(child.model, child.thinking) || undefined;
|
|
327
|
+
const childNestedChildren = nestedChildrenByParentStep.get(child.index)
|
|
328
|
+
?? (control.activeChildren.size === 1 && nestedChildren.length ? nestedChildren : undefined);
|
|
334
329
|
entries.push({
|
|
335
330
|
key: `foreground-active:${control.runId}:${child.index}`,
|
|
336
331
|
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
@@ -338,12 +333,10 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
338
333
|
...(modelThinking ? { modelThinking } : {}),
|
|
339
334
|
description: foregroundDescription(control, child.description),
|
|
340
335
|
startedAt: child.startedAt,
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
tokens: child.tokens ?? 0,
|
|
337
|
+
...(child.window !== undefined ? { window: child.window } : {}),
|
|
343
338
|
state: "running",
|
|
344
|
-
...(
|
|
345
|
-
? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
|
|
346
|
-
: control.activeChildren.size === 1 && control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
|
|
339
|
+
...(childNestedChildren?.length ? { nestedChildren: childNestedChildren } : {}),
|
|
347
340
|
});
|
|
348
341
|
}
|
|
349
342
|
continue;
|
|
@@ -366,10 +359,14 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
366
359
|
for (const job of state.asyncJobs.values()) {
|
|
367
360
|
if (!isActiveState(job.status)) continue;
|
|
368
361
|
const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
|
|
362
|
+
const linkedParentKey = linkedWorkflowParentKey(job.parentWorkflowRunId, activeWorkflowKeys);
|
|
369
363
|
if (job.mode === "workflow") {
|
|
370
364
|
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
365
|
+
const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
|
|
366
|
+
const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
|
|
371
367
|
entries.push({
|
|
372
368
|
key: `async:${job.asyncId}`,
|
|
369
|
+
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
373
370
|
workflowWrapper: true,
|
|
374
371
|
agent: "workflow",
|
|
375
372
|
description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
@@ -377,7 +374,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
377
374
|
tokens: job.totalTokens?.total ?? 0,
|
|
378
375
|
...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
|
|
379
376
|
state: job.status,
|
|
380
|
-
...(
|
|
377
|
+
...(workflowRows.length ? { workflowRows } : {}),
|
|
381
378
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
382
379
|
});
|
|
383
380
|
continue;
|
|
@@ -392,6 +389,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
392
389
|
if (!steps?.length) {
|
|
393
390
|
entries.push({
|
|
394
391
|
key: `async:${job.asyncId}`,
|
|
392
|
+
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
395
393
|
agent: job.mode ?? "subagent",
|
|
396
394
|
description: job.description,
|
|
397
395
|
startedAt,
|
|
@@ -409,6 +407,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
409
407
|
const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
|
|
410
408
|
entries.push({
|
|
411
409
|
key: `async:${job.asyncId}:${index}`,
|
|
410
|
+
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
412
411
|
agent: step.label ? `${step.label} (${step.agent})` : step.agent,
|
|
413
412
|
...(modelThinking ? { modelThinking } : {}),
|
|
414
413
|
description: step.description ?? job.description,
|
|
@@ -552,7 +551,12 @@ export class SubagentFleetStatus {
|
|
|
552
551
|
this.lastRenderKey = renderKey;
|
|
553
552
|
return;
|
|
554
553
|
}
|
|
555
|
-
if (renderKey === this.lastRenderKey)
|
|
554
|
+
if (renderKey === this.lastRenderKey) {
|
|
555
|
+
// Repaint anyway while anything is running so the wall-clock
|
|
556
|
+
// spinner animates between state changes (500ms tick).
|
|
557
|
+
if (this.entries.some((entry) => entry.state === "running")) this.tui?.requestRender();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
556
560
|
this.lastRenderKey = renderKey;
|
|
557
561
|
this.tui?.requestRender();
|
|
558
562
|
}
|
|
@@ -643,6 +647,10 @@ export class SubagentFleetStatus {
|
|
|
643
647
|
}
|
|
644
648
|
const roster = this.rosterKeys();
|
|
645
649
|
const selectedIndex = Math.max(0, roster.indexOf(this.selectedKey));
|
|
650
|
+
const rosterIndexByKey = new Map<string, number>();
|
|
651
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
652
|
+
if (!rosterIndexByKey.has(entry.key)) rosterIndexByKey.set(entry.key, index + 1);
|
|
653
|
+
}
|
|
646
654
|
const lines = [truncateToWidth(` ${theme.fg("dim", "↑↓/jk select · enter inspect · esc back")}`, width), ""];
|
|
647
655
|
lines.push(truncateToWidth(` ${this.bullet(0, selectedIndex, theme)} main`, width));
|
|
648
656
|
|
|
@@ -656,8 +664,8 @@ export class SubagentFleetStatus {
|
|
|
656
664
|
for (let index = start; index < start + visibleCount; index++) {
|
|
657
665
|
const row = tree[index]!;
|
|
658
666
|
if (row.kind === "owner" || row.kind === "child") {
|
|
659
|
-
const
|
|
660
|
-
lines.push(this.renderEntry(
|
|
667
|
+
const rosterIndex = rosterIndexByKey.get(row.entry.key) ?? 0;
|
|
668
|
+
lines.push(this.renderEntry(rosterIndex, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
|
|
661
669
|
} else if (row.kind === "workflow") {
|
|
662
670
|
lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
|
|
663
671
|
} else {
|
|
@@ -665,16 +673,16 @@ export class SubagentFleetStatus {
|
|
|
665
673
|
}
|
|
666
674
|
}
|
|
667
675
|
if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
|
|
668
|
-
this.renderProjectPaneSection(lines, selectedIndex, width, theme);
|
|
676
|
+
this.renderProjectPaneSection(lines, selectedIndex, width, theme, rosterIndexByKey);
|
|
669
677
|
return lines;
|
|
670
678
|
}
|
|
671
679
|
|
|
672
|
-
private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme): void {
|
|
680
|
+
private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme, rosterIndexByKey: ReadonlyMap<string, number>): void {
|
|
673
681
|
const entries = this.entries.filter((entry) => entry.surface === "project-pane");
|
|
674
682
|
if (!entries.length) return;
|
|
675
683
|
lines.push("", truncateToWidth(` ${theme.fg("dim", "project panes")}`, width));
|
|
676
684
|
for (const entry of entries) {
|
|
677
|
-
const rosterIndex =
|
|
685
|
+
const rosterIndex = rosterIndexByKey.get(entry.key) ?? 0;
|
|
678
686
|
lines.push(this.renderEntry(rosterIndex, selectedIndex, entry, width, theme));
|
|
679
687
|
}
|
|
680
688
|
}
|
|
@@ -703,16 +711,51 @@ export class SubagentFleetStatus {
|
|
|
703
711
|
return truncateToWidth(`${left}${theme.fg("dim", elapsed)}`, width);
|
|
704
712
|
}
|
|
705
713
|
|
|
706
|
-
private
|
|
714
|
+
private workflowRowGlyph(row: AsyncStatusWorkflowRow, theme: Theme): string {
|
|
715
|
+
if (!row.kind) return nestedStatusGlyph(row.state as FleetNestedRow["state"], theme);
|
|
716
|
+
const state = row.state as HostStepState;
|
|
717
|
+
if (state === "pending") return theme.fg("muted", "◦");
|
|
718
|
+
if (state === "running") return theme.fg("accent", "●");
|
|
719
|
+
if (state === "done") return row.verdict === "pass" ? theme.fg("success", "✓") : row.verdict === "fail" ? theme.fg("error", "✗") : theme.fg("warning", "■");
|
|
720
|
+
if (state === "error") return theme.fg("error", "✗");
|
|
721
|
+
return theme.fg("warning", "■");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private workflowRowStateLabel(row: AsyncStatusWorkflowRow, theme: Theme): string {
|
|
725
|
+
const state = row.kind ? hostStepVerdictLabel(row.state as HostStepState, row.verdict as HostStepVerdict | undefined) : row.state;
|
|
726
|
+
if (state === "running") return theme.fg("accent", state);
|
|
727
|
+
if (state === "pending" || state === "queued") return theme.fg("muted", state);
|
|
728
|
+
if (state === "pass" || state === "complete" || state === "completed") return theme.fg("success", state === "pass" ? "pass" : "complete");
|
|
729
|
+
if (state === "fail" || state === "failed" || state === "error") return theme.fg("error", state === "fail" ? "fail" : state);
|
|
730
|
+
return theme.fg("warning", state);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private renderWorkflowRow(row: AsyncStatusWorkflowRow, last: boolean, width: number, theme: Theme): string {
|
|
707
734
|
const marker = last ? "└─" : "├─";
|
|
708
735
|
const indent = " ";
|
|
709
736
|
if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} hidden workflow steps`)}`, width);
|
|
737
|
+
const context = contextModeLabel(row.context);
|
|
710
738
|
const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
|
|
711
739
|
const activity = row.activity ? ` · ${row.activity}` : "";
|
|
712
|
-
const
|
|
740
|
+
const kind = row.kind ? `${row.kind}: ` : "";
|
|
741
|
+
const hints = row.preflight ? [
|
|
742
|
+
row.preflight.mode ? `mode:${row.preflight.mode}` : undefined,
|
|
743
|
+
row.preflight.decision ? `decision:${row.preflight.decision}` : undefined,
|
|
744
|
+
row.preflight.claims?.length ? `claims:${row.preflight.claims.join(",")}` : undefined,
|
|
745
|
+
row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
|
|
746
|
+
row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
|
|
747
|
+
].filter((value): value is string => Boolean(value)).join(" · ") : "";
|
|
748
|
+
const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${context ? ` ${context}` : ""}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
|
|
713
749
|
const details = [
|
|
714
750
|
row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
|
|
715
751
|
row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
|
|
752
|
+
row.provider ? `provider:${row.provider}` : undefined,
|
|
753
|
+
row.role ? `role:${row.role}` : undefined,
|
|
754
|
+
row.target,
|
|
755
|
+
row.detail,
|
|
756
|
+
row.reasonCode ? `reason:${row.reasonCode}` : undefined,
|
|
757
|
+
row.freshness?.stale ? "stale" : row.freshness?.observedRef ? `ref:${row.freshness.observedRef}` : undefined,
|
|
758
|
+
row.reportPath ? `out:${hostStepReportName(row.reportPath)}` : undefined,
|
|
716
759
|
].filter(Boolean).join(" · ");
|
|
717
760
|
return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
|
|
718
761
|
}
|
|
@@ -767,12 +810,22 @@ export class SubagentFleetStatus {
|
|
|
767
810
|
Math.round((now - entry.startedAt) / 1000),
|
|
768
811
|
entry.tokens,
|
|
769
812
|
visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
|
|
813
|
+
row.kind,
|
|
770
814
|
row.name,
|
|
771
815
|
row.state,
|
|
816
|
+
row.context,
|
|
772
817
|
row.modelThinking,
|
|
773
818
|
row.activity,
|
|
774
819
|
row.startedAt,
|
|
775
820
|
row.tokens,
|
|
821
|
+
row.provider,
|
|
822
|
+
row.role,
|
|
823
|
+
row.verdict,
|
|
824
|
+
row.reasonCode,
|
|
825
|
+
row.detail,
|
|
826
|
+
row.target,
|
|
827
|
+
row.freshness,
|
|
828
|
+
row.reportPath,
|
|
776
829
|
row.overflow,
|
|
777
830
|
]),
|
|
778
831
|
nestedFleetRows(entry.nestedChildren, entry.parentKey ? 3 : 4).map((row) => [
|
|
@@ -807,7 +860,13 @@ export class SubagentFleetStatus {
|
|
|
807
860
|
|
|
808
861
|
private clearWidget(): void {
|
|
809
862
|
if (!this.widgetRegistered) return;
|
|
810
|
-
|
|
863
|
+
try {
|
|
864
|
+
this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
867
|
+
this.clearUiRegistration();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
811
870
|
this.widgetRegistered = false;
|
|
812
871
|
this.tui = undefined;
|
|
813
872
|
}
|
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
|
|
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
|
|
|
@@ -30,6 +30,37 @@ export function fuzzyFilter<T extends { name: string; description: string; model
|
|
|
30
30
|
.map((x) => x.item);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Remove a repeated job name from a child label when a safe separator follows it. */
|
|
34
|
+
export function stripRepeatedAgentPrefix(label: string, jobName: string | undefined): string {
|
|
35
|
+
const value = label.trim();
|
|
36
|
+
const prefix = jobName?.trim();
|
|
37
|
+
if (!prefix || !value.startsWith(prefix)) return value;
|
|
38
|
+
|
|
39
|
+
const suffix = value.slice(prefix.length);
|
|
40
|
+
if (!suffix || !/^(?::|·|\s)/u.test(suffix)) return value;
|
|
41
|
+
return suffix.replace(/^\s*(?::|·)?\s*/u, "").trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether a status label may omit the one logical step marker. */
|
|
45
|
+
export function shouldSuppressSingleStep(chainStepCount?: number, stepsTotal?: number): boolean {
|
|
46
|
+
return (chainStepCount ?? stepsTotal) === 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Add an Agent fraction only when visible candidates collide. */
|
|
50
|
+
export function withDuplicateLabelDiscriminators<T extends { index: number; displayName: string }>(
|
|
51
|
+
rows: readonly T[],
|
|
52
|
+
total: number,
|
|
53
|
+
): Array<T & { rowLabel: string }> {
|
|
54
|
+
const counts = new Map<string, number>();
|
|
55
|
+
for (const row of rows) counts.set(row.displayName, (counts.get(row.displayName) ?? 0) + 1);
|
|
56
|
+
return rows.map((row) => ({
|
|
57
|
+
...row,
|
|
58
|
+
rowLabel: (counts.get(row.displayName) ?? 0) > 1
|
|
59
|
+
? `Agent ${row.index + 1}/${total}: ${row.displayName}`
|
|
60
|
+
: row.displayName,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
export function pad(s: string, len: number): string {
|
|
34
65
|
const vis = visibleWidth(s);
|
|
35
66
|
return s + " ".repeat(Math.max(0, len - vis));
|