pi-subagents 0.35.0 → 0.36.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 +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
package/src/tui/fleet.ts
CHANGED
|
@@ -1,50 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
|
-
import type
|
|
3
|
-
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
|
+
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
6
|
+
import { formatDuration, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
7
|
+
import { RESULTS_DIR, type AsyncJobState, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
6
8
|
import { readStatus } from "../shared/utils.ts";
|
|
7
9
|
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
8
|
-
import { listAsyncRuns,
|
|
10
|
+
import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
11
|
+
import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
12
|
+
import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
|
|
13
|
+
import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
|
|
9
14
|
|
|
10
15
|
const REFRESH_MS = 750;
|
|
11
16
|
const MAX_RECENT_ASYNC_RUNS = 20;
|
|
12
17
|
const TRANSCRIPT_LINES = 200;
|
|
13
18
|
|
|
14
19
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
15
|
-
type
|
|
20
|
+
type FleetTui = {
|
|
21
|
+
terminal?: { rows: number };
|
|
22
|
+
requestRender(): void;
|
|
23
|
+
};
|
|
16
24
|
type AsyncStep = AsyncRunSummary["steps"][number];
|
|
17
25
|
|
|
18
|
-
export type FleetItem =
|
|
19
|
-
| { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control:
|
|
26
|
+
export type FleetItem = (
|
|
27
|
+
| { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control: ForegroundRunControl; activeChild?: ForegroundChildControl }
|
|
20
28
|
| { key: string; kind: "foreground-recent"; runId: string; index: number; agent: string; state: ForegroundResumeChild["status"]; updatedAt: number; run: ForegroundResumeRun; child: ForegroundResumeChild }
|
|
21
|
-
| { key: string; kind: "async"; runId: string; index?: number; agent: string; state: string; updatedAt: number; run: AsyncRunSummary; step?: AsyncStep }
|
|
29
|
+
| { key: string; kind: "async"; runId: string; index?: number; agent: string; state: string; updatedAt: number; run: AsyncRunSummary; step?: AsyncStep }
|
|
30
|
+
) & { description?: string };
|
|
22
31
|
|
|
23
32
|
export interface FleetSnapshot {
|
|
24
33
|
items: FleetItem[];
|
|
25
34
|
error?: string;
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
export interface FleetViewOptions {
|
|
38
|
+
asyncDirRoot?: string;
|
|
39
|
+
resultsDir?: string;
|
|
40
|
+
refreshMs?: number;
|
|
41
|
+
initialKey?: string;
|
|
42
|
+
markdownTheme?: MarkdownTheme;
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
|
|
29
46
|
return !currentSessionId || sessionId === currentSessionId;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
function trackedJobSummary(job: AsyncJobState): AsyncRunSummary {
|
|
33
|
-
const status = readStatus(job.asyncDir);
|
|
34
|
-
if (status) return summarizeAsyncStatus(job.asyncDir, status);
|
|
35
50
|
const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
|
|
36
51
|
return {
|
|
37
52
|
id: job.asyncId,
|
|
38
53
|
asyncDir: job.asyncDir,
|
|
39
54
|
...(job.sessionId ? { sessionId: job.sessionId } : {}),
|
|
40
55
|
state: job.status,
|
|
56
|
+
activityState: job.activityState,
|
|
57
|
+
lastActivityAt: job.lastActivityAt,
|
|
58
|
+
currentTool: job.currentTool,
|
|
59
|
+
currentToolStartedAt: job.currentToolStartedAt,
|
|
60
|
+
currentPath: job.currentPath,
|
|
61
|
+
turnCount: job.turnCount,
|
|
62
|
+
toolCount: job.toolCount,
|
|
63
|
+
steering: job.steering,
|
|
41
64
|
mode: job.mode ?? "single",
|
|
65
|
+
...(job.context ? { context: job.context } : {}),
|
|
66
|
+
...(job.cwd ? { cwd: job.cwd } : {}),
|
|
42
67
|
startedAt,
|
|
43
68
|
...(job.updatedAt !== undefined ? { lastUpdate: job.updatedAt } : {}),
|
|
69
|
+
...(job.timeoutMs !== undefined ? { timeoutMs: job.timeoutMs } : {}),
|
|
70
|
+
...(job.deadlineAt !== undefined ? { deadlineAt: job.deadlineAt } : {}),
|
|
71
|
+
...(job.timedOut !== undefined ? { timedOut: job.timedOut } : {}),
|
|
72
|
+
...(job.stopped !== undefined ? { stopped: job.stopped } : {}),
|
|
73
|
+
...(job.turnBudget ? { turnBudget: job.turnBudget } : {}),
|
|
74
|
+
...(job.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: job.turnBudgetExceeded } : {}),
|
|
75
|
+
...(job.wrapUpRequested !== undefined ? { wrapUpRequested: job.wrapUpRequested } : {}),
|
|
44
76
|
...(job.currentStep !== undefined ? { currentStep: job.currentStep } : {}),
|
|
45
77
|
...(job.chainStepCount !== undefined ? { chainStepCount: job.chainStepCount } : {}),
|
|
46
78
|
...(job.parallelGroups?.length ? { parallelGroups: job.parallelGroups } : {}),
|
|
47
|
-
steps: (job.steps ?? job.agents?.map((agent) => ({ agent, status: job.status === "queued" ? "pending" as const : job.status })) ?? []).map((step, index) => ({
|
|
79
|
+
steps: (job.steps ?? job.agents?.map((agent, index) => ({ agent, index, status: job.status === "queued" ? "pending" as const : job.status })) ?? []).map((step, index) => ({
|
|
48
80
|
...step,
|
|
49
81
|
index: step.index ?? index,
|
|
50
82
|
})),
|
|
@@ -52,13 +84,14 @@ function trackedJobSummary(job: AsyncJobState): AsyncRunSummary {
|
|
|
52
84
|
...(job.outputFile ? { outputFile: job.outputFile } : {}),
|
|
53
85
|
...(job.totalTokens ? { totalTokens: job.totalTokens } : {}),
|
|
54
86
|
...(job.sessionFile ? { sessionFile: job.sessionFile } : {}),
|
|
87
|
+
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
55
88
|
};
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
function asyncItems(run: AsyncRunSummary): FleetItem[] {
|
|
91
|
+
function asyncItems(run: AsyncRunSummary, description?: string): FleetItem[] {
|
|
59
92
|
const updatedAt = run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
60
93
|
if (run.steps.length === 0) {
|
|
61
|
-
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run }];
|
|
94
|
+
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
|
|
62
95
|
}
|
|
63
96
|
return run.steps.map((step) => ({
|
|
64
97
|
key: `async:${run.id}:${step.index}`,
|
|
@@ -70,6 +103,7 @@ function asyncItems(run: AsyncRunSummary): FleetItem[] {
|
|
|
70
103
|
updatedAt: step.lastActivityAt ?? updatedAt,
|
|
71
104
|
run,
|
|
72
105
|
step,
|
|
106
|
+
...(description ? { description } : {}),
|
|
73
107
|
}));
|
|
74
108
|
}
|
|
75
109
|
|
|
@@ -81,6 +115,23 @@ export function collectFleetSnapshot(
|
|
|
81
115
|
const activeForegroundIds = new Set<string>();
|
|
82
116
|
for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
|
|
83
117
|
activeForegroundIds.add(control.runId);
|
|
118
|
+
if (control.activeChildren) {
|
|
119
|
+
for (const child of [...control.activeChildren.values()].sort((left, right) => left.index - right.index)) {
|
|
120
|
+
items.push({
|
|
121
|
+
key: `foreground-active:${control.runId}:${child.index}`,
|
|
122
|
+
kind: "foreground-active",
|
|
123
|
+
runId: control.runId,
|
|
124
|
+
index: child.index,
|
|
125
|
+
agent: child.agent,
|
|
126
|
+
state: "running",
|
|
127
|
+
updatedAt: child.updatedAt,
|
|
128
|
+
control,
|
|
129
|
+
activeChild: child,
|
|
130
|
+
...(child.description ? { description: child.description } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
84
135
|
items.push({
|
|
85
136
|
key: `foreground-active:${control.runId}:${control.currentIndex ?? 0}`,
|
|
86
137
|
kind: "foreground-active",
|
|
@@ -90,12 +141,14 @@ export function collectFleetSnapshot(
|
|
|
90
141
|
state: "running",
|
|
91
142
|
updatedAt: control.updatedAt,
|
|
92
143
|
control,
|
|
144
|
+
...(control.description ? { description: control.description } : {}),
|
|
93
145
|
});
|
|
94
146
|
}
|
|
95
147
|
|
|
96
148
|
let error: string | undefined;
|
|
97
149
|
try {
|
|
98
150
|
let runs: AsyncRunSummary[];
|
|
151
|
+
const descriptions = new Map<string, string>();
|
|
99
152
|
if (options.asyncDirRoot !== undefined) {
|
|
100
153
|
runs = listAsyncRuns(options.asyncDirRoot, {
|
|
101
154
|
...(state.currentSessionId ? { sessionId: state.currentSessionId } : {}),
|
|
@@ -113,12 +166,13 @@ export function collectFleetSnapshot(
|
|
|
113
166
|
for (const job of [...active, ...recent]) {
|
|
114
167
|
try {
|
|
115
168
|
runs.push(trackedJobSummary(job));
|
|
169
|
+
if (job.description) descriptions.set(job.asyncId, job.description);
|
|
116
170
|
} catch (cause) {
|
|
117
171
|
error = `Failed to inspect async run '${job.asyncId}': ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
118
172
|
}
|
|
119
173
|
}
|
|
120
174
|
}
|
|
121
|
-
for (const run of runs) items.push(...asyncItems(run));
|
|
175
|
+
for (const run of runs) items.push(...asyncItems(run, descriptions.get(run.id)));
|
|
122
176
|
} catch (cause) {
|
|
123
177
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
124
178
|
}
|
|
@@ -154,17 +208,18 @@ function statusGlyph(item: FleetItem, theme: Theme): string {
|
|
|
154
208
|
|
|
155
209
|
function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-active" }>): string[] {
|
|
156
210
|
const { control } = item;
|
|
211
|
+
const live = item.activeChild ?? control;
|
|
157
212
|
const lines = [
|
|
158
213
|
`Run: ${item.runId}`,
|
|
159
214
|
"Source: foreground",
|
|
160
215
|
`State: running`,
|
|
161
216
|
`Mode: ${control.mode}`,
|
|
162
217
|
item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
|
|
163
|
-
`Started: ${new Date(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
218
|
+
`Started: ${new Date(live.startedAt).toISOString()}`,
|
|
219
|
+
live.currentTool ? `Current tool: ${live.currentTool}${live.currentPath ? ` · ${shortenPath(live.currentPath)}` : ""}` : undefined,
|
|
220
|
+
live.turnCount !== undefined ? `Turns: ${live.turnCount}` : undefined,
|
|
221
|
+
live.toolCount !== undefined ? `Tools: ${live.toolCount}` : undefined,
|
|
222
|
+
live.tokens !== undefined ? `Tokens: ${formatTokens(live.tokens)}` : undefined,
|
|
168
223
|
"",
|
|
169
224
|
"Transcript",
|
|
170
225
|
"Live foreground output remains in the expanded subagent tool result. Persisted output and session paths appear here after the child settles.",
|
|
@@ -180,7 +235,7 @@ function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-rec
|
|
|
180
235
|
"Source: foreground",
|
|
181
236
|
`State: ${child.status}`,
|
|
182
237
|
`Mode: ${run.mode}`,
|
|
183
|
-
`Child: ${child.index} (${child.agent})`,
|
|
238
|
+
`Child: ${child.index} (${child.agent})${contextModeLabel(child.context) ? ` ${contextModeLabel(child.context)}` : ""}`,
|
|
184
239
|
`Updated: ${new Date(child.updatedAt ?? run.updatedAt).toISOString()}`,
|
|
185
240
|
outputPath ? `Output: ${outputPath}` : undefined,
|
|
186
241
|
child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
|
|
@@ -206,8 +261,8 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
|
|
|
206
261
|
`Run: ${item.runId}`,
|
|
207
262
|
"Source: async",
|
|
208
263
|
`State: ${item.state}`,
|
|
209
|
-
`Mode: ${item.run.mode}`,
|
|
210
|
-
item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
|
|
264
|
+
`Mode: ${item.run.mode}${contextModeLabel(item.run.context) ? ` ${contextModeLabel(item.run.context)}` : ""}`,
|
|
265
|
+
item.index !== undefined ? `Child: ${item.index} (${item.agent})${contextModeLabel(item.step?.context) ? ` ${contextModeLabel(item.step?.context)}` : ""}` : `Agent: ${item.agent}${contextModeLabel(item.run.context) ? ` ${contextModeLabel(item.run.context)}` : ""}`,
|
|
211
266
|
outputPath ? `Output: ${outputPath}` : undefined,
|
|
212
267
|
item.step?.sessionFile ? `Session: ${item.step.sessionFile}` : item.run.sessionFile ? `Session: ${item.run.sessionFile}` : undefined,
|
|
213
268
|
"",
|
|
@@ -227,6 +282,116 @@ function detailLines(item: FleetItem | undefined, error: string | undefined): st
|
|
|
227
282
|
return lines;
|
|
228
283
|
}
|
|
229
284
|
|
|
285
|
+
function uniquePaths(values: Array<string | undefined>): string[] {
|
|
286
|
+
return [...new Set(values.filter((value): value is string => Boolean(value)).map((value) => path.resolve(value)))];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
|
|
290
|
+
return getArtifactsDir(
|
|
291
|
+
state.parentSessionFile ?? null,
|
|
292
|
+
cwd,
|
|
293
|
+
state.artifactDirPreference ?? "project",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
|
|
298
|
+
if (item.kind === "foreground-active") {
|
|
299
|
+
const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
|
|
300
|
+
return {
|
|
301
|
+
path: getArtifactPaths(artifactsRoot, item.runId, item.agent, item.index ?? 0).transcriptPath,
|
|
302
|
+
trustedRoots: [artifactsRoot],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (item.kind === "foreground-recent") {
|
|
306
|
+
if (!item.child.transcriptPath) return undefined;
|
|
307
|
+
const transcriptPath = path.isAbsolute(item.child.transcriptPath)
|
|
308
|
+
? item.child.transcriptPath
|
|
309
|
+
: path.resolve(item.run.cwd, item.child.transcriptPath);
|
|
310
|
+
return {
|
|
311
|
+
path: transcriptPath,
|
|
312
|
+
trustedRoots: uniquePaths([
|
|
313
|
+
fleetArtifactsRoot(state, item.run.cwd),
|
|
314
|
+
fleetArtifactsRoot(state, state.baseCwd),
|
|
315
|
+
]),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const step = item.step ?? (item.run.steps.length === 1 ? item.run.steps[0] : undefined);
|
|
319
|
+
if (!step?.transcriptPath) return undefined;
|
|
320
|
+
const transcriptPath = path.isAbsolute(step.transcriptPath)
|
|
321
|
+
? step.transcriptPath
|
|
322
|
+
: path.resolve(item.run.asyncDir, step.transcriptPath);
|
|
323
|
+
const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
|
|
324
|
+
return {
|
|
325
|
+
path: transcriptPath,
|
|
326
|
+
trustedRoots: uniquePaths([
|
|
327
|
+
item.run.asyncDir,
|
|
328
|
+
fleetArtifactsRoot(state, state.baseCwd),
|
|
329
|
+
trackedJob?.cwd ? fleetArtifactsRoot(state, trackedJob.cwd) : undefined,
|
|
330
|
+
]),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function itemContext(item: FleetItem): string | undefined {
|
|
335
|
+
if (item.kind === "async") return contextModeLabel(item.step?.context ?? item.run.context);
|
|
336
|
+
if (item.kind === "foreground-recent") return contextModeLabel(item.child.context);
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function itemMode(item: FleetItem): string {
|
|
341
|
+
return item.kind === "foreground-active" ? item.control.mode : item.run.mode;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function itemSource(item: FleetItem): string {
|
|
345
|
+
if (item.kind === "async") return "background";
|
|
346
|
+
return item.kind === "foreground-active" ? "foreground · live" : "foreground · recent";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function itemStats(item: FleetItem): string[] {
|
|
350
|
+
let model: string | undefined;
|
|
351
|
+
let tokens: number | undefined;
|
|
352
|
+
let tools: number | undefined;
|
|
353
|
+
let durationMs: number | undefined;
|
|
354
|
+
if (item.kind === "foreground-active") {
|
|
355
|
+
const live = item.activeChild ?? item.control;
|
|
356
|
+
tokens = live.tokens;
|
|
357
|
+
tools = live.toolCount;
|
|
358
|
+
durationMs = Math.max(0, Date.now() - live.startedAt);
|
|
359
|
+
} else if (item.kind === "foreground-recent") {
|
|
360
|
+
tokens = item.child.tokens;
|
|
361
|
+
tools = item.child.toolCount;
|
|
362
|
+
} else {
|
|
363
|
+
model = item.step?.model;
|
|
364
|
+
tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
|
|
365
|
+
tools = item.step?.toolCount ?? (item.index === undefined ? item.run.toolCount : undefined);
|
|
366
|
+
const terminalRun = item.state !== "queued" && item.state !== "running" && item.state !== "pending";
|
|
367
|
+
const endTime = item.run.endedAt ?? (terminalRun ? item.run.lastUpdate : undefined) ?? Date.now();
|
|
368
|
+
durationMs = item.step?.durationMs ?? Math.max(0, endTime - item.run.startedAt);
|
|
369
|
+
}
|
|
370
|
+
return [
|
|
371
|
+
model,
|
|
372
|
+
tokens !== undefined ? `${formatTokens(tokens)} tok` : undefined,
|
|
373
|
+
tools !== undefined ? `${tools} tool${tools === 1 ? "" : "s"}` : undefined,
|
|
374
|
+
durationMs !== undefined ? formatDuration(durationMs) : undefined,
|
|
375
|
+
].filter((value): value is string => Boolean(value));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function structuredHeader(item: FleetItem, width: number, theme: Theme, conversationState: string): string[] {
|
|
379
|
+
const lines: string[] = [];
|
|
380
|
+
lines.push(rightAligned(` ${statusGlyph(item, theme)} ${theme.bold(item.agent)}`, theme.fg("dim", item.state), width));
|
|
381
|
+
const child = item.index !== undefined ? ` · child ${item.index + 1}` : "";
|
|
382
|
+
const context = itemContext(item);
|
|
383
|
+
const identity = `${itemSource(item)} · ${item.runId.slice(0, 8)}${child} · ${itemMode(item)}${context ? ` ${context}` : ""}`;
|
|
384
|
+
lines.push(` ${theme.fg("dim", identity)}`);
|
|
385
|
+
const stats = itemStats(item);
|
|
386
|
+
if (stats.length) lines.push(` ${theme.fg("muted", stats.join(" · "))}`);
|
|
387
|
+
if (item.description) {
|
|
388
|
+
const task = item.description.replace(/\s+/g, " ").trim();
|
|
389
|
+
lines.push(` ${theme.fg("dim", "Task")} ${task}`);
|
|
390
|
+
}
|
|
391
|
+
lines.push(`${theme.fg("accent", "Conversation")} ${theme.fg("dim", `· ${conversationState}`)}`);
|
|
392
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
393
|
+
}
|
|
394
|
+
|
|
230
395
|
function fit(text: string, width: number): string {
|
|
231
396
|
const clipped = truncateToWidth(text, Math.max(0, width));
|
|
232
397
|
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
@@ -238,6 +403,29 @@ function rightAligned(left: string, right: string, width: number): string {
|
|
|
238
403
|
return fit(left, leftWidth) + " ".repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);
|
|
239
404
|
}
|
|
240
405
|
|
|
406
|
+
interface FleetDetailSections {
|
|
407
|
+
header: string[];
|
|
408
|
+
body: string[];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
interface FleetTranscriptCache {
|
|
412
|
+
path: string;
|
|
413
|
+
fingerprint: string;
|
|
414
|
+
width: number;
|
|
415
|
+
expandedTools: boolean;
|
|
416
|
+
transcript: FleetTranscript;
|
|
417
|
+
body: string[];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function transcriptFingerprint(filePath: string): string {
|
|
421
|
+
try {
|
|
422
|
+
const stat = fs.statSync(filePath);
|
|
423
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
424
|
+
} catch {
|
|
425
|
+
return "missing";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
241
429
|
export class SubagentFleetComponent implements Component {
|
|
242
430
|
private snapshot: FleetSnapshot = { items: [] };
|
|
243
431
|
private selected = 0;
|
|
@@ -245,27 +433,33 @@ export class SubagentFleetComponent implements Component {
|
|
|
245
433
|
private detailScroll = 0;
|
|
246
434
|
private detailAutoFollow = true;
|
|
247
435
|
private detailLineCount = 0;
|
|
436
|
+
private detailViewportHeight = 8;
|
|
248
437
|
private bodyHeight = 8;
|
|
438
|
+
private expandedTools = false;
|
|
439
|
+
private transcriptCache: FleetTranscriptCache | undefined;
|
|
249
440
|
private disposed = false;
|
|
250
441
|
private readonly timer: ReturnType<typeof setInterval>;
|
|
251
|
-
private readonly tui:
|
|
442
|
+
private readonly tui: FleetTui;
|
|
252
443
|
private readonly theme: Theme;
|
|
444
|
+
private readonly markdownTheme: MarkdownTheme;
|
|
253
445
|
private readonly state: SubagentState;
|
|
254
446
|
private readonly done: (result: undefined) => void;
|
|
255
|
-
private readonly options:
|
|
447
|
+
private readonly options: FleetViewOptions;
|
|
256
448
|
|
|
257
449
|
constructor(
|
|
258
|
-
tui:
|
|
450
|
+
tui: FleetTui,
|
|
259
451
|
theme: Theme,
|
|
260
452
|
state: SubagentState,
|
|
261
453
|
done: (result: undefined) => void,
|
|
262
|
-
options:
|
|
454
|
+
options: FleetViewOptions = {},
|
|
263
455
|
) {
|
|
264
456
|
this.tui = tui;
|
|
265
457
|
this.theme = theme;
|
|
458
|
+
this.markdownTheme = options.markdownTheme ?? getMarkdownTheme();
|
|
266
459
|
this.state = state;
|
|
267
460
|
this.done = done;
|
|
268
461
|
this.options = options;
|
|
462
|
+
this.selectedKey = options.initialKey;
|
|
269
463
|
this.refresh();
|
|
270
464
|
this.timer = setInterval(() => {
|
|
271
465
|
if (this.disposed) return;
|
|
@@ -291,30 +485,35 @@ export class SubagentFleetComponent implements Component {
|
|
|
291
485
|
this.tui.requestRender();
|
|
292
486
|
}
|
|
293
487
|
|
|
488
|
+
private scrollDetail(delta: number): void {
|
|
489
|
+
const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);
|
|
490
|
+
this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));
|
|
491
|
+
this.detailAutoFollow = this.detailScroll >= maxScroll;
|
|
492
|
+
this.tui.requestRender();
|
|
493
|
+
}
|
|
494
|
+
|
|
294
495
|
handleInput(data: string): void {
|
|
295
496
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
|
|
296
497
|
this.done(undefined);
|
|
297
498
|
return;
|
|
298
499
|
}
|
|
500
|
+
if (matchesKey(data, Key.shift("k"))) return this.scrollDetail(-1);
|
|
501
|
+
if (matchesKey(data, Key.shift("j"))) return this.scrollDetail(1);
|
|
299
502
|
if (matchesKey(data, "up") || matchesKey(data, "k")) return this.moveSelection(-1);
|
|
300
503
|
if (matchesKey(data, "down") || matchesKey(data, "j")) return this.moveSelection(1);
|
|
301
504
|
if (matchesKey(data, "home")) return this.moveSelection(-this.snapshot.items.length);
|
|
302
505
|
if (matchesKey(data, "end")) return this.moveSelection(this.snapshot.items.length);
|
|
303
|
-
if (matchesKey(data, "pageUp"))
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
if (matchesKey(data, "pageDown")) {
|
|
310
|
-
const maxScroll = Math.max(0, this.detailLineCount - this.bodyHeight);
|
|
311
|
-
this.detailScroll = Math.min(maxScroll, this.detailScroll + this.bodyHeight);
|
|
312
|
-
this.detailAutoFollow = this.detailScroll >= maxScroll;
|
|
506
|
+
if (matchesKey(data, "pageUp")) return this.scrollDetail(-this.detailViewportHeight);
|
|
507
|
+
if (matchesKey(data, "pageDown")) return this.scrollDetail(this.detailViewportHeight);
|
|
508
|
+
if (data.toLowerCase() === "r") {
|
|
509
|
+
this.transcriptCache = undefined;
|
|
510
|
+
this.refresh();
|
|
313
511
|
this.tui.requestRender();
|
|
314
512
|
return;
|
|
315
513
|
}
|
|
316
|
-
if (data.toLowerCase() === "
|
|
317
|
-
this.
|
|
514
|
+
if (data.toLowerCase() === "x" || matchesKey(data, "ctrl+o")) {
|
|
515
|
+
this.expandedTools = !this.expandedTools;
|
|
516
|
+
this.transcriptCache = undefined;
|
|
318
517
|
this.tui.requestRender();
|
|
319
518
|
}
|
|
320
519
|
}
|
|
@@ -325,16 +524,56 @@ export class SubagentFleetComponent implements Component {
|
|
|
325
524
|
return this.snapshot.items.slice(start, start + this.bodyHeight).map((item, offset) => {
|
|
326
525
|
const index = start + offset;
|
|
327
526
|
const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
527
|
+
const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
|
|
528
|
+
const agent = index === this.selected ? this.theme.bold(item.agent) : item.agent;
|
|
529
|
+
const identity = item.description?.replace(/\s+/g, " ").trim() || item.runId.slice(0, 8);
|
|
530
|
+
const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
|
|
331
531
|
return rightAligned(left, this.theme.fg("dim", item.state), width);
|
|
332
532
|
});
|
|
333
533
|
}
|
|
334
534
|
|
|
335
|
-
private
|
|
535
|
+
private renderedTranscript(target: { path: string; trustedRoots: string[] }, width: number): { transcript: FleetTranscript; body: string[] } {
|
|
536
|
+
const fingerprint = `${target.trustedRoots.join("\0")}|${transcriptFingerprint(target.path)}`;
|
|
537
|
+
if (this.transcriptCache
|
|
538
|
+
&& this.transcriptCache.path === target.path
|
|
539
|
+
&& this.transcriptCache.fingerprint === fingerprint
|
|
540
|
+
&& this.transcriptCache.width === width
|
|
541
|
+
&& this.transcriptCache.expandedTools === this.expandedTools) {
|
|
542
|
+
return { transcript: this.transcriptCache.transcript, body: [...this.transcriptCache.body] };
|
|
543
|
+
}
|
|
544
|
+
const transcript = readFleetTranscript(target.path, { trustedRoots: target.trustedRoots });
|
|
545
|
+
const body = transcript.events.length > 0
|
|
546
|
+
? renderFleetTranscript(transcript, width, this.theme, this.markdownTheme, { expandedTools: this.expandedTools })
|
|
547
|
+
: [];
|
|
548
|
+
this.transcriptCache = { path: target.path, fingerprint, width, expandedTools: this.expandedTools, transcript, body };
|
|
549
|
+
return { transcript, body: [...body] };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private wrappedDetail(width: number): FleetDetailSections {
|
|
336
553
|
const selected = this.snapshot.items[this.selected];
|
|
554
|
+
let transcriptWarning: string | undefined;
|
|
555
|
+
if (selected) {
|
|
556
|
+
const target = transcriptTarget(selected, this.state);
|
|
557
|
+
if (target) {
|
|
558
|
+
const { transcript, body } = this.renderedTranscript(target, width);
|
|
559
|
+
transcriptWarning = transcript.warning;
|
|
560
|
+
if (transcript.events.length > 0) {
|
|
561
|
+
if (this.snapshot.error) body.unshift(this.theme.fg("warning", `Fleet scan warning: ${this.snapshot.error}`), "");
|
|
562
|
+
const latest = transcript.events.at(-1);
|
|
563
|
+
const conversationState = latest?.kind === "assistant"
|
|
564
|
+
? "assistant response"
|
|
565
|
+
: latest?.kind === "user"
|
|
566
|
+
? "supervisor message"
|
|
567
|
+
: latest?.kind === "tool"
|
|
568
|
+
? `${latest.name} · ${latest.status}`
|
|
569
|
+
: "activity";
|
|
570
|
+
return { header: structuredHeader(selected, width, this.theme, conversationState), body };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
337
575
|
const raw = detailLines(selected, this.snapshot.error);
|
|
576
|
+
if (transcriptWarning) raw.unshift(`Transcript preview warning: ${transcriptWarning}`, "");
|
|
338
577
|
const lines: string[] = [];
|
|
339
578
|
for (const line of raw) {
|
|
340
579
|
const styled = /^(Run|State|Mode|Source|Child|Agent):/.test(line)
|
|
@@ -343,11 +582,13 @@ export class SubagentFleetComponent implements Component {
|
|
|
343
582
|
? this.theme.fg("accent", line)
|
|
344
583
|
: /^(Output|Session|Transcript file|Artifacts):/.test(line)
|
|
345
584
|
? this.theme.fg("muted", line)
|
|
346
|
-
: line
|
|
585
|
+
: /^Transcript preview warning:/.test(line)
|
|
586
|
+
? this.theme.fg("warning", line)
|
|
587
|
+
: line;
|
|
347
588
|
const wrapped = wrapTextWithAnsi(styled, Math.max(1, width));
|
|
348
589
|
lines.push(...(wrapped.length ? wrapped : [""]));
|
|
349
590
|
}
|
|
350
|
-
return lines;
|
|
591
|
+
return { header: [], body: lines };
|
|
351
592
|
}
|
|
352
593
|
|
|
353
594
|
render(width: number): string[] {
|
|
@@ -358,14 +599,24 @@ export class SubagentFleetComponent implements Component {
|
|
|
358
599
|
const rosterWidth = Math.max(22, Math.min(46, Math.floor((innerWidth - 1) * 0.38)));
|
|
359
600
|
const detailWidth = Math.max(1, innerWidth - rosterWidth - 1);
|
|
360
601
|
const roster = this.rosterLines(rosterWidth);
|
|
361
|
-
const
|
|
362
|
-
this.
|
|
363
|
-
|
|
602
|
+
const detail = this.wrappedDetail(detailWidth);
|
|
603
|
+
const detailHeader = detail.header.slice(0, Math.max(0, this.bodyHeight - 1));
|
|
604
|
+
this.detailViewportHeight = Math.max(1, this.bodyHeight - detailHeader.length);
|
|
605
|
+
this.detailLineCount = detail.body.length;
|
|
606
|
+
const maxDetailScroll = Math.max(0, detail.body.length - this.detailViewportHeight);
|
|
364
607
|
if (this.detailAutoFollow) this.detailScroll = maxDetailScroll;
|
|
365
608
|
else if (this.detailScroll > maxDetailScroll) this.detailScroll = maxDetailScroll;
|
|
366
|
-
const visibleDetails =
|
|
609
|
+
const visibleDetails = [
|
|
610
|
+
...detailHeader,
|
|
611
|
+
...detail.body.slice(this.detailScroll, this.detailScroll + this.detailViewportHeight),
|
|
612
|
+
];
|
|
367
613
|
const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
368
|
-
|
|
614
|
+
const selected = this.snapshot.items[this.selected];
|
|
615
|
+
const title = ` ${this.theme.bold("Subagent fleet inspector")} ${this.theme.fg("dim", "· inspection only · live")}`;
|
|
616
|
+
const selectedStatus = selected
|
|
617
|
+
? `${statusGlyph(selected, this.theme)} ${selected.agent} · ${selected.state} `
|
|
618
|
+
: this.theme.fg("dim", "no children ");
|
|
619
|
+
lines.push(this.theme.fg("border", "│") + rightAligned(title, selectedStatus, innerWidth) + this.theme.fg("border", "│"));
|
|
369
620
|
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┬${"─".repeat(detailWidth)}┤`));
|
|
370
621
|
for (let index = 0; index < this.bodyHeight; index++) {
|
|
371
622
|
lines.push(
|
|
@@ -378,13 +629,14 @@ export class SubagentFleetComponent implements Component {
|
|
|
378
629
|
}
|
|
379
630
|
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┴${"─".repeat(detailWidth)}┤`));
|
|
380
631
|
const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
|
|
381
|
-
const footer = ` ↑↓/jk
|
|
632
|
+
const footer = ` ↑↓/jk agent · ⇧k/⇧j scroll · PgUp/PgDn page · x/Ctrl+O tools · r refresh · Esc close · ${position}`;
|
|
382
633
|
lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
|
|
383
634
|
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
384
635
|
return lines.map((line) => truncateToWidth(line, width));
|
|
385
636
|
}
|
|
386
637
|
|
|
387
638
|
invalidate(): void {
|
|
639
|
+
this.transcriptCache = undefined;
|
|
388
640
|
this.refresh();
|
|
389
641
|
}
|
|
390
642
|
|
|
@@ -394,12 +646,19 @@ export class SubagentFleetComponent implements Component {
|
|
|
394
646
|
}
|
|
395
647
|
}
|
|
396
648
|
|
|
397
|
-
export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentState): Promise<void> {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
649
|
+
export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentState, options: FleetViewOptions = {}): Promise<void> {
|
|
650
|
+
const wasOpen = state.fleetInspectorOpen === true;
|
|
651
|
+
state.fleetInspectorOpen = true;
|
|
652
|
+
if (typeof ctx.ui.setWidget === "function") ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
653
|
+
try {
|
|
654
|
+
await ctx.ui.custom<undefined>(
|
|
655
|
+
(tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done, options),
|
|
656
|
+
{
|
|
657
|
+
overlay: true,
|
|
658
|
+
overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
} finally {
|
|
662
|
+
state.fleetInspectorOpen = wasOpen;
|
|
663
|
+
}
|
|
405
664
|
}
|