pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
package/src/tui/fleet.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
5
|
+
import { RESULTS_DIR, type AsyncJobState, type ForegroundResumeChild, type ForegroundResumeRun, type SubagentState } from "../shared/types.ts";
|
|
6
|
+
import { readStatus } from "../shared/utils.ts";
|
|
7
|
+
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
8
|
+
import { listAsyncRuns, summarizeAsyncStatus, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
9
|
+
|
|
10
|
+
const REFRESH_MS = 750;
|
|
11
|
+
const MAX_RECENT_ASYNC_RUNS = 20;
|
|
12
|
+
const TRANSCRIPT_LINES = 200;
|
|
13
|
+
|
|
14
|
+
type Theme = ExtensionContext["ui"]["theme"];
|
|
15
|
+
type ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
16
|
+
type AsyncStep = AsyncRunSummary["steps"][number];
|
|
17
|
+
|
|
18
|
+
export type FleetItem =
|
|
19
|
+
| { key: string; kind: "foreground-active"; runId: string; index?: number; agent: string; state: "running"; updatedAt: number; control: ForegroundControl }
|
|
20
|
+
| { 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 };
|
|
22
|
+
|
|
23
|
+
export interface FleetSnapshot {
|
|
24
|
+
items: FleetItem[];
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function belongsToCurrentSession(sessionId: string | undefined, currentSessionId: string | null): boolean {
|
|
29
|
+
return !currentSessionId || sessionId === currentSessionId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function trackedJobSummary(job: AsyncJobState): AsyncRunSummary {
|
|
33
|
+
const status = readStatus(job.asyncDir);
|
|
34
|
+
if (status) return summarizeAsyncStatus(job.asyncDir, status);
|
|
35
|
+
const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
|
|
36
|
+
return {
|
|
37
|
+
id: job.asyncId,
|
|
38
|
+
asyncDir: job.asyncDir,
|
|
39
|
+
...(job.sessionId ? { sessionId: job.sessionId } : {}),
|
|
40
|
+
state: job.status,
|
|
41
|
+
mode: job.mode ?? "single",
|
|
42
|
+
startedAt,
|
|
43
|
+
...(job.updatedAt !== undefined ? { lastUpdate: job.updatedAt } : {}),
|
|
44
|
+
...(job.currentStep !== undefined ? { currentStep: job.currentStep } : {}),
|
|
45
|
+
...(job.chainStepCount !== undefined ? { chainStepCount: job.chainStepCount } : {}),
|
|
46
|
+
...(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) => ({
|
|
48
|
+
...step,
|
|
49
|
+
index: step.index ?? index,
|
|
50
|
+
})),
|
|
51
|
+
...(job.sessionDir ? { sessionDir: job.sessionDir } : {}),
|
|
52
|
+
...(job.outputFile ? { outputFile: job.outputFile } : {}),
|
|
53
|
+
...(job.totalTokens ? { totalTokens: job.totalTokens } : {}),
|
|
54
|
+
...(job.sessionFile ? { sessionFile: job.sessionFile } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asyncItems(run: AsyncRunSummary): FleetItem[] {
|
|
59
|
+
const updatedAt = run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
60
|
+
if (run.steps.length === 0) {
|
|
61
|
+
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run }];
|
|
62
|
+
}
|
|
63
|
+
return run.steps.map((step) => ({
|
|
64
|
+
key: `async:${run.id}:${step.index}`,
|
|
65
|
+
kind: "async" as const,
|
|
66
|
+
runId: run.id,
|
|
67
|
+
index: step.index,
|
|
68
|
+
agent: step.label ? `${step.label} (${step.agent})` : step.agent,
|
|
69
|
+
state: step.status,
|
|
70
|
+
updatedAt: step.lastActivityAt ?? updatedAt,
|
|
71
|
+
run,
|
|
72
|
+
step,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function collectFleetSnapshot(
|
|
77
|
+
state: SubagentState,
|
|
78
|
+
options: { asyncDirRoot?: string; resultsDir?: string; limit?: number } = {},
|
|
79
|
+
): FleetSnapshot {
|
|
80
|
+
const items: FleetItem[] = [];
|
|
81
|
+
const activeForegroundIds = new Set<string>();
|
|
82
|
+
for (const control of [...state.foregroundControls.values()].sort((left, right) => right.updatedAt - left.updatedAt)) {
|
|
83
|
+
activeForegroundIds.add(control.runId);
|
|
84
|
+
items.push({
|
|
85
|
+
key: `foreground-active:${control.runId}:${control.currentIndex ?? 0}`,
|
|
86
|
+
kind: "foreground-active",
|
|
87
|
+
runId: control.runId,
|
|
88
|
+
...(control.currentIndex !== undefined ? { index: control.currentIndex } : {}),
|
|
89
|
+
agent: control.currentAgent ?? control.mode,
|
|
90
|
+
state: "running",
|
|
91
|
+
updatedAt: control.updatedAt,
|
|
92
|
+
control,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let error: string | undefined;
|
|
97
|
+
try {
|
|
98
|
+
let runs: AsyncRunSummary[];
|
|
99
|
+
if (options.asyncDirRoot !== undefined) {
|
|
100
|
+
runs = listAsyncRuns(options.asyncDirRoot, {
|
|
101
|
+
...(state.currentSessionId ? { sessionId: state.currentSessionId } : {}),
|
|
102
|
+
limit: options.limit ?? MAX_RECENT_ASYNC_RUNS,
|
|
103
|
+
resultsDir: options.resultsDir ?? RESULTS_DIR,
|
|
104
|
+
reconcile: false,
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
const tracked = [...(state.fleetJobs ?? state.asyncJobs).values()]
|
|
108
|
+
.filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
|
|
109
|
+
const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
|
|
110
|
+
const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
|
|
111
|
+
const recent = tracked.filter((job) => job.status !== "queued" && job.status !== "running").sort(byUpdate).slice(0, options.limit ?? MAX_RECENT_ASYNC_RUNS);
|
|
112
|
+
runs = [];
|
|
113
|
+
for (const job of [...active, ...recent]) {
|
|
114
|
+
try {
|
|
115
|
+
runs.push(trackedJobSummary(job));
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
error = `Failed to inspect async run '${job.asyncId}': ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const run of runs) items.push(...asyncItems(run));
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const recentForeground = [...(state.foregroundRuns?.values() ?? [])]
|
|
127
|
+
.filter((run) => belongsToCurrentSession(run.sessionId, state.currentSessionId) && !activeForegroundIds.has(run.runId))
|
|
128
|
+
.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
129
|
+
for (const run of recentForeground) {
|
|
130
|
+
for (const child of run.children) {
|
|
131
|
+
items.push({
|
|
132
|
+
key: `foreground-recent:${run.runId}:${child.index}`,
|
|
133
|
+
kind: "foreground-recent",
|
|
134
|
+
runId: run.runId,
|
|
135
|
+
index: child.index,
|
|
136
|
+
agent: child.agent,
|
|
137
|
+
state: child.status,
|
|
138
|
+
updatedAt: child.updatedAt ?? run.updatedAt,
|
|
139
|
+
run,
|
|
140
|
+
child,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { items, ...(error ? { error } : {}) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function statusGlyph(item: FleetItem, theme: Theme): string {
|
|
148
|
+
if (item.state === "running") return theme.fg("accent", "●");
|
|
149
|
+
if (item.state === "queued" || item.state === "pending") return theme.fg("muted", "◦");
|
|
150
|
+
if (item.state === "complete" || item.state === "completed") return theme.fg("success", "✓");
|
|
151
|
+
if (item.state === "paused" || item.state === "stopped" || item.state === "detached") return theme.fg("warning", "■");
|
|
152
|
+
return theme.fg("error", "✗");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-active" }>): string[] {
|
|
156
|
+
const { control } = item;
|
|
157
|
+
const lines = [
|
|
158
|
+
`Run: ${item.runId}`,
|
|
159
|
+
"Source: foreground",
|
|
160
|
+
`State: running`,
|
|
161
|
+
`Mode: ${control.mode}`,
|
|
162
|
+
item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
|
|
163
|
+
`Started: ${new Date(control.startedAt).toISOString()}`,
|
|
164
|
+
control.currentTool ? `Current tool: ${control.currentTool}${control.currentPath ? ` · ${shortenPath(control.currentPath)}` : ""}` : undefined,
|
|
165
|
+
control.turnCount !== undefined ? `Turns: ${control.turnCount}` : undefined,
|
|
166
|
+
control.toolCount !== undefined ? `Tools: ${control.toolCount}` : undefined,
|
|
167
|
+
control.tokens !== undefined ? `Tokens: ${formatTokens(control.tokens)}` : undefined,
|
|
168
|
+
"",
|
|
169
|
+
"Transcript",
|
|
170
|
+
"Live foreground output remains in the expanded subagent tool result. Persisted output and session paths appear here after the child settles.",
|
|
171
|
+
];
|
|
172
|
+
return lines.filter((line): line is string => line !== undefined);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function foregroundRecentDetail(item: Extract<FleetItem, { kind: "foreground-recent" }>): string[] {
|
|
176
|
+
const { child, run } = item;
|
|
177
|
+
const outputPath = child.artifactPaths?.outputPath ?? child.savedOutputPath;
|
|
178
|
+
const lines = [
|
|
179
|
+
`Run: ${item.runId}`,
|
|
180
|
+
"Source: foreground",
|
|
181
|
+
`State: ${child.status}`,
|
|
182
|
+
`Mode: ${run.mode}`,
|
|
183
|
+
`Child: ${child.index} (${child.agent})`,
|
|
184
|
+
`Updated: ${new Date(child.updatedAt ?? run.updatedAt).toISOString()}`,
|
|
185
|
+
outputPath ? `Output: ${outputPath}` : undefined,
|
|
186
|
+
child.sessionFile ? `Session: ${child.sessionFile}` : undefined,
|
|
187
|
+
child.transcriptPath ? `Transcript file: ${child.transcriptPath}` : undefined,
|
|
188
|
+
child.error ? `Error: ${child.error}` : undefined,
|
|
189
|
+
child.outputSaveError ? `Output warning: ${child.outputSaveError}` : undefined,
|
|
190
|
+
child.transcriptError ? `Transcript warning: ${child.transcriptError}` : undefined,
|
|
191
|
+
"",
|
|
192
|
+
"Result transcript tail",
|
|
193
|
+
];
|
|
194
|
+
const outputLines = (child.finalOutput ?? "").split(/\r?\n/).filter((line) => line.trim()).slice(-TRANSCRIPT_LINES);
|
|
195
|
+
lines.push(...(outputLines.length ? outputLines : ["(no recovered output available)"]));
|
|
196
|
+
return lines.filter((line): line is string => line !== undefined);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function asyncDetail(item: Extract<FleetItem, { kind: "async" }>): string[] {
|
|
200
|
+
const status = readStatus(item.run.asyncDir);
|
|
201
|
+
if (status) {
|
|
202
|
+
return formatAsyncRunTranscript(status, item.run.asyncDir, { index: item.index, lines: TRANSCRIPT_LINES }).split("\n");
|
|
203
|
+
}
|
|
204
|
+
const outputPath = item.index !== undefined ? path.join(item.run.asyncDir, `output-${item.index}.log`) : undefined;
|
|
205
|
+
return [
|
|
206
|
+
`Run: ${item.runId}`,
|
|
207
|
+
"Source: async",
|
|
208
|
+
`State: ${item.state}`,
|
|
209
|
+
`Mode: ${item.run.mode}`,
|
|
210
|
+
item.index !== undefined ? `Child: ${item.index} (${item.agent})` : `Agent: ${item.agent}`,
|
|
211
|
+
outputPath ? `Output: ${outputPath}` : undefined,
|
|
212
|
+
item.step?.sessionFile ? `Session: ${item.step.sessionFile}` : item.run.sessionFile ? `Session: ${item.run.sessionFile}` : undefined,
|
|
213
|
+
"",
|
|
214
|
+
"Transcript",
|
|
215
|
+
"(status is no longer available)",
|
|
216
|
+
].filter((line): line is string => line !== undefined);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function detailLines(item: FleetItem | undefined, error: string | undefined): string[] {
|
|
220
|
+
if (!item) return [error ? `Fleet scan failed: ${error}` : "No current-session foreground or recent async children.", "", "New runs appear here automatically while this inspector remains open."];
|
|
221
|
+
const lines = item.kind === "foreground-active"
|
|
222
|
+
? foregroundActiveDetail(item)
|
|
223
|
+
: item.kind === "foreground-recent"
|
|
224
|
+
? foregroundRecentDetail(item)
|
|
225
|
+
: asyncDetail(item);
|
|
226
|
+
if (error) lines.unshift(`Fleet scan warning: ${error}`, "");
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function fit(text: string, width: number): string {
|
|
231
|
+
const clipped = truncateToWidth(text, Math.max(0, width));
|
|
232
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function rightAligned(left: string, right: string, width: number): string {
|
|
236
|
+
const rightWidth = visibleWidth(right);
|
|
237
|
+
const leftWidth = Math.max(0, width - rightWidth - 1);
|
|
238
|
+
return fit(left, leftWidth) + " ".repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class SubagentFleetComponent implements Component {
|
|
242
|
+
private snapshot: FleetSnapshot = { items: [] };
|
|
243
|
+
private selected = 0;
|
|
244
|
+
private selectedKey: string | undefined;
|
|
245
|
+
private detailScroll = 0;
|
|
246
|
+
private detailAutoFollow = true;
|
|
247
|
+
private detailLineCount = 0;
|
|
248
|
+
private bodyHeight = 8;
|
|
249
|
+
private disposed = false;
|
|
250
|
+
private readonly timer: ReturnType<typeof setInterval>;
|
|
251
|
+
private readonly tui: TUI;
|
|
252
|
+
private readonly theme: Theme;
|
|
253
|
+
private readonly state: SubagentState;
|
|
254
|
+
private readonly done: (result: undefined) => void;
|
|
255
|
+
private readonly options: { asyncDirRoot?: string; resultsDir?: string; refreshMs?: number };
|
|
256
|
+
|
|
257
|
+
constructor(
|
|
258
|
+
tui: TUI,
|
|
259
|
+
theme: Theme,
|
|
260
|
+
state: SubagentState,
|
|
261
|
+
done: (result: undefined) => void,
|
|
262
|
+
options: { asyncDirRoot?: string; resultsDir?: string; refreshMs?: number } = {},
|
|
263
|
+
) {
|
|
264
|
+
this.tui = tui;
|
|
265
|
+
this.theme = theme;
|
|
266
|
+
this.state = state;
|
|
267
|
+
this.done = done;
|
|
268
|
+
this.options = options;
|
|
269
|
+
this.refresh();
|
|
270
|
+
this.timer = setInterval(() => {
|
|
271
|
+
if (this.disposed) return;
|
|
272
|
+
this.refresh();
|
|
273
|
+
this.tui.requestRender();
|
|
274
|
+
}, options.refreshMs ?? REFRESH_MS);
|
|
275
|
+
this.timer.unref?.();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private refresh(): void {
|
|
279
|
+
const previousKey = this.snapshot.items[this.selected]?.key ?? this.selectedKey;
|
|
280
|
+
this.snapshot = collectFleetSnapshot(this.state, this.options);
|
|
281
|
+
const preserved = previousKey ? this.snapshot.items.findIndex((item) => item.key === previousKey) : -1;
|
|
282
|
+
this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, this.snapshot.items.length - 1));
|
|
283
|
+
this.selectedKey = this.snapshot.items[this.selected]?.key;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private moveSelection(delta: number): void {
|
|
287
|
+
if (this.snapshot.items.length === 0) return;
|
|
288
|
+
this.selected = Math.max(0, Math.min(this.snapshot.items.length - 1, this.selected + delta));
|
|
289
|
+
this.selectedKey = this.snapshot.items[this.selected]?.key;
|
|
290
|
+
this.detailAutoFollow = true;
|
|
291
|
+
this.tui.requestRender();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
handleInput(data: string): void {
|
|
295
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "q")) {
|
|
296
|
+
this.done(undefined);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) return this.moveSelection(-1);
|
|
300
|
+
if (matchesKey(data, "down") || matchesKey(data, "j")) return this.moveSelection(1);
|
|
301
|
+
if (matchesKey(data, "home")) return this.moveSelection(-this.snapshot.items.length);
|
|
302
|
+
if (matchesKey(data, "end")) return this.moveSelection(this.snapshot.items.length);
|
|
303
|
+
if (matchesKey(data, "pageUp")) {
|
|
304
|
+
this.detailAutoFollow = false;
|
|
305
|
+
this.detailScroll = Math.max(0, Math.min(this.detailScroll, Math.max(0, this.detailLineCount - this.bodyHeight)) - this.bodyHeight);
|
|
306
|
+
this.tui.requestRender();
|
|
307
|
+
return;
|
|
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;
|
|
313
|
+
this.tui.requestRender();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (data.toLowerCase() === "r") {
|
|
317
|
+
this.refresh();
|
|
318
|
+
this.tui.requestRender();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private rosterLines(width: number): string[] {
|
|
323
|
+
if (this.snapshot.items.length === 0) return [this.theme.fg("dim", "No tracked children")];
|
|
324
|
+
const start = Math.max(0, Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.snapshot.items.length - this.bodyHeight)));
|
|
325
|
+
return this.snapshot.items.slice(start, start + this.bodyHeight).map((item, offset) => {
|
|
326
|
+
const index = start + offset;
|
|
327
|
+
const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
|
|
328
|
+
const child = item.index !== undefined ? `:${item.index + 1}` : "";
|
|
329
|
+
const source = item.kind === "async" ? "async" : item.kind === "foreground-active" ? "live" : "recent";
|
|
330
|
+
const left = `${marker} ${statusGlyph(item, this.theme)} ${source} ${item.runId.slice(0, 8)}${child} ${item.agent}`;
|
|
331
|
+
return rightAligned(left, this.theme.fg("dim", item.state), width);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private wrappedDetail(width: number): string[] {
|
|
336
|
+
const selected = this.snapshot.items[this.selected];
|
|
337
|
+
const raw = detailLines(selected, this.snapshot.error);
|
|
338
|
+
const lines: string[] = [];
|
|
339
|
+
for (const line of raw) {
|
|
340
|
+
const styled = /^(Run|State|Mode|Source|Child|Agent):/.test(line)
|
|
341
|
+
? this.theme.bold(line)
|
|
342
|
+
: /^(Transcript|Result transcript tail)/.test(line)
|
|
343
|
+
? this.theme.fg("accent", line)
|
|
344
|
+
: /^(Output|Session|Transcript file|Artifacts):/.test(line)
|
|
345
|
+
? this.theme.fg("muted", line)
|
|
346
|
+
: line;
|
|
347
|
+
const wrapped = wrapTextWithAnsi(styled, Math.max(1, width));
|
|
348
|
+
lines.push(...(wrapped.length ? wrapped : [""]));
|
|
349
|
+
}
|
|
350
|
+
return lines;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
render(width: number): string[] {
|
|
354
|
+
if (width < 36) return [truncateToWidth("Subagent fleet needs at least 36 columns. Esc closes.", width)];
|
|
355
|
+
const innerWidth = width - 2;
|
|
356
|
+
const rows = this.tui.terminal?.rows ?? 32;
|
|
357
|
+
this.bodyHeight = Math.max(2, Math.min(30, Math.floor(rows * 0.85) - 6));
|
|
358
|
+
const rosterWidth = Math.max(22, Math.min(46, Math.floor((innerWidth - 1) * 0.38)));
|
|
359
|
+
const detailWidth = Math.max(1, innerWidth - rosterWidth - 1);
|
|
360
|
+
const roster = this.rosterLines(rosterWidth);
|
|
361
|
+
const details = this.wrappedDetail(detailWidth);
|
|
362
|
+
this.detailLineCount = details.length;
|
|
363
|
+
const maxDetailScroll = Math.max(0, details.length - this.bodyHeight);
|
|
364
|
+
if (this.detailAutoFollow) this.detailScroll = maxDetailScroll;
|
|
365
|
+
else if (this.detailScroll > maxDetailScroll) this.detailScroll = maxDetailScroll;
|
|
366
|
+
const visibleDetails = details.slice(this.detailScroll, this.detailScroll + this.bodyHeight);
|
|
367
|
+
const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
368
|
+
lines.push(this.theme.fg("border", "│") + fit(` ${this.theme.bold("Subagent fleet")} ${this.theme.fg("dim", "· inspection only · live refresh")}`, innerWidth) + this.theme.fg("border", "│"));
|
|
369
|
+
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┬${"─".repeat(detailWidth)}┤`));
|
|
370
|
+
for (let index = 0; index < this.bodyHeight; index++) {
|
|
371
|
+
lines.push(
|
|
372
|
+
this.theme.fg("border", "│")
|
|
373
|
+
+ fit(roster[index] ?? "", rosterWidth)
|
|
374
|
+
+ this.theme.fg("border", "│")
|
|
375
|
+
+ fit(visibleDetails[index] ?? "", detailWidth)
|
|
376
|
+
+ this.theme.fg("border", "│"),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┴${"─".repeat(detailWidth)}┤`));
|
|
380
|
+
const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
|
|
381
|
+
const footer = ` ↑↓/jk child · PgUp/PgDn transcript · r refresh · Esc close · ${position}`;
|
|
382
|
+
lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
|
|
383
|
+
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
384
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
invalidate(): void {
|
|
388
|
+
this.refresh();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
dispose(): void {
|
|
392
|
+
this.disposed = true;
|
|
393
|
+
clearInterval(this.timer);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentState): Promise<void> {
|
|
398
|
+
await ctx.ui.custom<undefined>(
|
|
399
|
+
(tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done),
|
|
400
|
+
{
|
|
401
|
+
overlay: true,
|
|
402
|
+
overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
}
|