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.
Files changed (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. 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 { 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";
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, summarizeAsyncStatus, type AsyncRunSummary } from "../runs/background/async-status.ts";
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 ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
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: ForegroundControl }
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(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,
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: 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: { asyncDirRoot?: string; resultsDir?: string; refreshMs?: number };
447
+ private readonly options: FleetViewOptions;
256
448
 
257
449
  constructor(
258
- tui: TUI,
450
+ tui: FleetTui,
259
451
  theme: Theme,
260
452
  state: SubagentState,
261
453
  done: (result: undefined) => void,
262
- options: { asyncDirRoot?: string; resultsDir?: string; refreshMs?: number } = {},
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
- 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;
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() === "r") {
317
- this.refresh();
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 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}`;
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 wrappedDetail(width: number): string[] {
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 details = this.wrappedDetail(detailWidth);
362
- this.detailLineCount = details.length;
363
- const maxDetailScroll = Math.max(0, details.length - this.bodyHeight);
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 = details.slice(this.detailScroll, this.detailScroll + this.bodyHeight);
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
- 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", "│"));
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 child · PgUp/PgDn transcript · r refresh · Esc close · ${position}`;
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
- 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
- );
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
  }