@apholdings/jensen-code 0.0.4 → 0.0.6

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 (161) hide show
  1. package/CHANGELOG.md +3061 -3061
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +6 -6
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts +17 -6
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +55 -28
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +10 -0
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/modes/interactive/components/assistant-message.d.ts +1 -6
  18. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  19. package/dist/modes/interactive/components/assistant-message.js +10 -40
  20. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  21. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  22. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  23. package/dist/modes/interactive/components/custom-editor.js +5 -0
  24. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  25. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/tool-execution.js +1 -2
  27. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  28. package/dist/modes/interactive/components/top-bar.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/top-bar.js +1 -1
  30. package/dist/modes/interactive/components/top-bar.js.map +1 -1
  31. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/user-message.js +1 -1
  33. package/dist/modes/interactive/components/user-message.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts +6 -3
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +204 -86
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/utils/frontmatter.d.ts.map +1 -1
  39. package/dist/utils/frontmatter.js +8 -4
  40. package/dist/utils/frontmatter.js.map +1 -1
  41. package/dist/utils/tools-manager.d.ts.map +1 -1
  42. package/dist/utils/tools-manager.js +2 -2
  43. package/dist/utils/tools-manager.js.map +1 -1
  44. package/docs/custom-provider.md +592 -592
  45. package/docs/session.md +412 -412
  46. package/examples/extensions/osgrep.ts +643 -0
  47. package/examples/extensions/subagent/agents.ts +150 -37
  48. package/examples/extensions/subagent/index.ts +634 -513
  49. package/package.json +3 -3
  50. package/examples/README.md +0 -25
  51. package/examples/extensions/README.md +0 -206
  52. package/examples/extensions/antigravity-image-gen.ts +0 -415
  53. package/examples/extensions/auto-commit-on-exit.ts +0 -49
  54. package/examples/extensions/bash-spawn-hook.ts +0 -30
  55. package/examples/extensions/bookmark.ts +0 -50
  56. package/examples/extensions/built-in-tool-renderer.ts +0 -246
  57. package/examples/extensions/claude-rules.ts +0 -86
  58. package/examples/extensions/commands.ts +0 -72
  59. package/examples/extensions/confirm-destructive.ts +0 -59
  60. package/examples/extensions/custom-compaction.ts +0 -114
  61. package/examples/extensions/custom-footer.ts +0 -64
  62. package/examples/extensions/custom-header.ts +0 -73
  63. package/examples/extensions/custom-provider-anthropic/index.ts +0 -604
  64. package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
  65. package/examples/extensions/custom-provider-anthropic/package.json +0 -19
  66. package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -349
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
  68. package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
  69. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -345
  70. package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
  71. package/examples/extensions/dirty-repo-guard.ts +0 -56
  72. package/examples/extensions/doom-overlay/README.md +0 -46
  73. package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
  74. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  75. package/examples/extensions/doom-overlay/doom/build.sh +0 -152
  76. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
  77. package/examples/extensions/doom-overlay/doom-component.ts +0 -132
  78. package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
  79. package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
  80. package/examples/extensions/doom-overlay/index.ts +0 -74
  81. package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
  82. package/examples/extensions/dynamic-resources/SKILL.md +0 -8
  83. package/examples/extensions/dynamic-resources/dynamic.json +0 -79
  84. package/examples/extensions/dynamic-resources/dynamic.md +0 -5
  85. package/examples/extensions/dynamic-resources/index.ts +0 -15
  86. package/examples/extensions/dynamic-tools.ts +0 -74
  87. package/examples/extensions/event-bus.ts +0 -43
  88. package/examples/extensions/file-trigger.ts +0 -41
  89. package/examples/extensions/git-checkpoint.ts +0 -53
  90. package/examples/extensions/handoff.ts +0 -150
  91. package/examples/extensions/hello.ts +0 -25
  92. package/examples/extensions/inline-bash.ts +0 -94
  93. package/examples/extensions/input-transform.ts +0 -43
  94. package/examples/extensions/interactive-shell.ts +0 -196
  95. package/examples/extensions/mac-system-theme.ts +0 -47
  96. package/examples/extensions/message-renderer.ts +0 -59
  97. package/examples/extensions/minimal-mode.ts +0 -426
  98. package/examples/extensions/modal-editor.ts +0 -85
  99. package/examples/extensions/model-status.ts +0 -31
  100. package/examples/extensions/notify.ts +0 -55
  101. package/examples/extensions/overlay-qa-tests.ts +0 -1348
  102. package/examples/extensions/overlay-test.ts +0 -150
  103. package/examples/extensions/permission-gate.ts +0 -34
  104. package/examples/extensions/pirate.ts +0 -47
  105. package/examples/extensions/plan-mode/README.md +0 -65
  106. package/examples/extensions/plan-mode/index.ts +0 -340
  107. package/examples/extensions/plan-mode/utils.ts +0 -168
  108. package/examples/extensions/preset.ts +0 -398
  109. package/examples/extensions/protected-paths.ts +0 -30
  110. package/examples/extensions/provider-payload.ts +0 -14
  111. package/examples/extensions/qna.ts +0 -119
  112. package/examples/extensions/question.ts +0 -264
  113. package/examples/extensions/questionnaire.ts +0 -427
  114. package/examples/extensions/rainbow-editor.ts +0 -88
  115. package/examples/extensions/reload-runtime.ts +0 -37
  116. package/examples/extensions/rpc-demo.ts +0 -124
  117. package/examples/extensions/sandbox/index.ts +0 -318
  118. package/examples/extensions/sandbox/package-lock.json +0 -92
  119. package/examples/extensions/sandbox/package.json +0 -19
  120. package/examples/extensions/send-user-message.ts +0 -97
  121. package/examples/extensions/session-name.ts +0 -27
  122. package/examples/extensions/shutdown-command.ts +0 -63
  123. package/examples/extensions/snake.ts +0 -343
  124. package/examples/extensions/space-invaders.ts +0 -560
  125. package/examples/extensions/ssh.ts +0 -220
  126. package/examples/extensions/status-line.ts +0 -40
  127. package/examples/extensions/subagent/README.md +0 -172
  128. package/examples/extensions/subagent/agents/planner.md +0 -37
  129. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  130. package/examples/extensions/subagent/agents/scout.md +0 -50
  131. package/examples/extensions/subagent/agents/worker.md +0 -24
  132. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  133. package/examples/extensions/subagent/prompts/implement.md +0 -10
  134. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  135. package/examples/extensions/summarize.ts +0 -195
  136. package/examples/extensions/system-prompt-header.ts +0 -17
  137. package/examples/extensions/timed-confirm.ts +0 -70
  138. package/examples/extensions/titlebar-spinner.ts +0 -58
  139. package/examples/extensions/todo.ts +0 -299
  140. package/examples/extensions/tool-override.ts +0 -143
  141. package/examples/extensions/tools.ts +0 -146
  142. package/examples/extensions/trigger-compact.ts +0 -40
  143. package/examples/extensions/truncated-tool.ts +0 -192
  144. package/examples/extensions/widget-placement.ts +0 -17
  145. package/examples/extensions/with-deps/index.ts +0 -32
  146. package/examples/extensions/with-deps/package-lock.json +0 -31
  147. package/examples/extensions/with-deps/package.json +0 -22
  148. package/examples/rpc-extension-ui.ts +0 -632
  149. package/examples/sdk/01-minimal.ts +0 -22
  150. package/examples/sdk/02-custom-model.ts +0 -49
  151. package/examples/sdk/03-custom-prompt.ts +0 -55
  152. package/examples/sdk/04-skills.ts +0 -46
  153. package/examples/sdk/05-tools.ts +0 -56
  154. package/examples/sdk/06-extensions.ts +0 -88
  155. package/examples/sdk/07-context-files.ts +0 -40
  156. package/examples/sdk/08-prompt-templates.ts +0 -47
  157. package/examples/sdk/09-api-keys-and-oauth.ts +0 -48
  158. package/examples/sdk/10-settings.ts +0 -51
  159. package/examples/sdk/11-sessions.ts +0 -48
  160. package/examples/sdk/12-full-control.ts +0 -82
  161. package/examples/sdk/README.md +0 -145
@@ -1,15 +1,5 @@
1
1
  /**
2
2
  * Subagent Tool - Delegate tasks to specialized agents
3
- *
4
- * Spawns a separate `pi` process for each subagent invocation,
5
- * giving it an isolated context window.
6
- *
7
- * Supports three modes:
8
- * - Single: { agent: "name", task: "..." }
9
- * - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
10
- * - Chain: { chain: [{ agent: "name", task: "... {previous} ..." }, ...] }
11
- *
12
- * Uses JSON mode to capture structured output from subagents.
13
3
  */
14
4
 
15
5
  import { spawn } from "node:child_process";
@@ -19,15 +9,101 @@ import * as path from "node:path";
19
9
  import type { AgentToolResult } from "@apholdings/jensen-agent-core";
20
10
  import type { Message } from "@apholdings/jensen-ai";
21
11
  import { StringEnum } from "@apholdings/jensen-ai";
22
- import { type ExtensionAPI, getMarkdownTheme } from "@apholdings/jensen-code";
12
+ import {
13
+ APP_NAME,
14
+ type ExtensionAPI,
15
+ type ExtensionContext,
16
+ getMarkdownTheme,
17
+ type Theme,
18
+ type ToolDefinition,
19
+ type ToolRenderResultOptions,
20
+ } from "@apholdings/jensen-code";
23
21
  import { Container, Markdown, Spacer, Text } from "@apholdings/jensen-tui";
24
- import { Type } from "@sinclair/typebox";
25
- import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
22
+ import { type Static, Type } from "@sinclair/typebox";
23
+ import {
24
+ type AgentConfig,
25
+ type AgentDiscoveryError,
26
+ type AgentScope,
27
+ discoverAgents,
28
+ findDiscoveryErrorForAgent,
29
+ } from "./agents.js";
26
30
 
27
31
  const MAX_PARALLEL_TASKS = 8;
28
32
  const MAX_CONCURRENCY = 4;
29
33
  const COLLAPSED_ITEM_COUNT = 10;
30
34
 
35
+ interface UsageStats {
36
+ input: number;
37
+ output: number;
38
+ cacheRead: number;
39
+ cacheWrite: number;
40
+ cost: number;
41
+ contextTokens: number;
42
+ turns: number;
43
+ }
44
+
45
+ type FailureStage = "lookup" | "discovery" | "launch" | "provider" | "result";
46
+
47
+ export interface SubagentInvocation {
48
+ command: string;
49
+ args: string[];
50
+ cwd: string;
51
+ displayCommand: string;
52
+ }
53
+
54
+ interface ExtractedFinalOutput {
55
+ text: string;
56
+ reason?: string;
57
+ }
58
+
59
+ interface SingleResult {
60
+ agent: string;
61
+ agentSource: "user" | "project" | "unknown";
62
+ task: string;
63
+ exitCode: number;
64
+ messages: Message[];
65
+ stderr: string;
66
+ usage: UsageStats;
67
+ model?: string;
68
+ stopReason?: string;
69
+ errorMessage?: string;
70
+ step?: number;
71
+ failureStage?: FailureStage;
72
+ diagnosticMessage?: string;
73
+ invocation?: SubagentInvocation;
74
+ }
75
+
76
+ interface SubagentDetails {
77
+ mode: "single" | "parallel" | "chain";
78
+ agentScope: AgentScope;
79
+ projectAgentsDir: string | null;
80
+ discoveryErrors: AgentDiscoveryError[];
81
+ results: SingleResult[];
82
+ }
83
+
84
+ type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, unknown> };
85
+
86
+ interface SubagentRunRequest {
87
+ invocation: SubagentInvocation;
88
+ agent: AgentConfig;
89
+ onMessage: (message: Message) => void;
90
+ signal?: AbortSignal;
91
+ }
92
+
93
+ interface SubagentRunResult {
94
+ exitCode: number;
95
+ stderr: string;
96
+ launchError?: string;
97
+ }
98
+
99
+ type SubagentParamsType = Static<typeof SubagentParams>;
100
+
101
+ function textContent(text: string) {
102
+ return [{ type: "text" as const, text }];
103
+ }
104
+
105
+ export type SubagentRunner = (request: SubagentRunRequest) => Promise<SubagentRunResult>;
106
+
31
107
  function formatTokens(count: number): string {
32
108
  if (count < 1000) return count.toString();
33
109
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -35,18 +111,7 @@ function formatTokens(count: number): string {
35
111
  return `${(count / 1000000).toFixed(1)}M`;
36
112
  }
37
113
 
38
- function formatUsageStats(
39
- usage: {
40
- input: number;
41
- output: number;
42
- cacheRead: number;
43
- cacheWrite: number;
44
- cost: number;
45
- contextTokens?: number;
46
- turns?: number;
47
- },
48
- model?: string,
49
- ): string {
114
+ function formatUsageStats(usage: UsageStats, model?: string): string {
50
115
  const parts: string[] = [];
51
116
  if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
52
117
  if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
@@ -54,9 +119,7 @@ function formatUsageStats(
54
119
  if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
55
120
  if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
56
121
  if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
57
- if (usage.contextTokens && usage.contextTokens > 0) {
58
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
59
- }
122
+ if (usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
60
123
  if (model) parts.push(model);
61
124
  return parts.join(" ");
62
125
  }
@@ -66,9 +129,9 @@ function formatToolCall(
66
129
  args: Record<string, unknown>,
67
130
  themeFg: (color: any, text: string) => string,
68
131
  ): string {
69
- const shortenPath = (p: string) => {
132
+ const shortenPath = (filePath: string) => {
70
133
  const home = os.homedir();
71
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
134
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
72
135
  };
73
136
 
74
137
  switch (toolName) {
@@ -122,71 +185,227 @@ function formatToolCall(
122
185
  );
123
186
  }
124
187
  default: {
125
- const argsStr = JSON.stringify(args);
126
- const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
188
+ const argsString = JSON.stringify(args);
189
+ const preview = argsString.length > 50 ? `${argsString.slice(0, 50)}...` : argsString;
127
190
  return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
128
191
  }
129
192
  }
130
193
  }
131
194
 
132
- interface UsageStats {
133
- input: number;
134
- output: number;
135
- cacheRead: number;
136
- cacheWrite: number;
137
- cost: number;
138
- contextTokens: number;
139
- turns: number;
140
- }
141
-
142
- interface SingleResult {
143
- agent: string;
144
- agentSource: "user" | "project" | "unknown";
145
- task: string;
146
- exitCode: number;
147
- messages: Message[];
148
- stderr: string;
149
- usage: UsageStats;
150
- model?: string;
151
- stopReason?: string;
152
- errorMessage?: string;
153
- step?: number;
195
+ function trimOutput(value: string): string {
196
+ return value.replace(/\s+$/u, "");
154
197
  }
155
198
 
156
- interface SubagentDetails {
157
- mode: "single" | "parallel" | "chain";
158
- agentScope: AgentScope;
159
- projectAgentsDir: string | null;
160
- results: SingleResult[];
161
- }
199
+ export function extractFinalOutput(messages: Message[]): ExtractedFinalOutput {
200
+ let sawAssistantMessage = false;
201
+ let sawAssistantWithoutText = false;
202
+ let sawAssistantWithEmptyText = false;
162
203
 
163
- function getFinalOutput(messages: Message[]): string {
164
204
  for (let i = messages.length - 1; i >= 0; i--) {
165
- const msg = messages[i];
166
- if (msg.role === "assistant") {
167
- for (const part of msg.content) {
168
- if (part.type === "text") return part.text;
169
- }
205
+ const message = messages[i];
206
+ if (message.role !== "assistant") continue;
207
+ sawAssistantMessage = true;
208
+ const textParts = message.content
209
+ .filter((part): part is Extract<Message["content"][number], { type: "text" }> => part.type === "text")
210
+ .map((part) => part.text);
211
+ if (textParts.length === 0) {
212
+ sawAssistantWithoutText = true;
213
+ continue;
214
+ }
215
+
216
+ const combinedText = trimOutput(textParts.join(""));
217
+ if (combinedText.length > 0) {
218
+ return { text: combinedText };
170
219
  }
220
+
221
+ sawAssistantWithEmptyText = true;
171
222
  }
172
- return "";
173
- }
174
223
 
175
- type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
224
+ if (!sawAssistantMessage) {
225
+ return { text: "", reason: "no assistant message was emitted by the child session" };
226
+ }
227
+ if (sawAssistantWithoutText) {
228
+ return { text: "", reason: "the final assistant message contained no text parts" };
229
+ }
230
+ if (sawAssistantWithEmptyText) {
231
+ return { text: "", reason: "the final assistant message only contained empty text" };
232
+ }
233
+ return { text: "", reason: "the child session ended without a final assistant text response" };
234
+ }
176
235
 
177
236
  function getDisplayItems(messages: Message[]): DisplayItem[] {
178
237
  const items: DisplayItem[] = [];
179
- for (const msg of messages) {
180
- if (msg.role === "assistant") {
181
- for (const part of msg.content) {
182
- if (part.type === "text") items.push({ type: "text", text: part.text });
183
- else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
238
+ for (const message of messages) {
239
+ if (message.role !== "assistant") continue;
240
+ for (const part of message.content) {
241
+ if (part.type === "text") {
242
+ if (part.text.trim().length > 0) {
243
+ items.push({ type: "text", text: part.text });
244
+ }
245
+ } else if (part.type === "toolCall") {
246
+ items.push({ type: "toolCall", name: part.name, args: part.arguments });
184
247
  }
185
248
  }
186
249
  }
187
250
  return items;
188
251
  }
189
252
 
253
+ function normalizeWorkingDirectory(defaultCwd: string, cwd?: string): string {
254
+ return path.normalize(path.resolve(cwd ?? defaultCwd));
255
+ }
256
+
257
+ function resolveCliCommandPrefix(): { command: string; prefixArgs: string[] } {
258
+ const cliEntry = process.argv[1];
259
+ if (typeof cliEntry === "string" && cliEntry.length > 0) {
260
+ const resolvedCliEntry = path.resolve(cliEntry);
261
+ if (cliEntry !== "-" && !cliEntry.startsWith("-") && fs.existsSync(resolvedCliEntry)) {
262
+ return {
263
+ command: process.execPath,
264
+ prefixArgs: [...process.execArgv, resolvedCliEntry],
265
+ };
266
+ }
267
+ }
268
+ return { command: process.execPath, prefixArgs: [] };
269
+ }
270
+
271
+ export function buildSubagentInvocation(
272
+ defaultCwd: string,
273
+ agent: AgentConfig,
274
+ task: string,
275
+ cwd?: string,
276
+ ): SubagentInvocation {
277
+ const { command, prefixArgs } = resolveCliCommandPrefix();
278
+ const args = [...prefixArgs, "--mode", "json", "-p", "--no-session"];
279
+ if (agent.model) args.push("--model", agent.model);
280
+ if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
281
+ args.push(`Task: ${task}`);
282
+
283
+ return {
284
+ command,
285
+ args,
286
+ cwd: normalizeWorkingDirectory(defaultCwd, cwd),
287
+ displayCommand: [command, ...args].join(" "),
288
+ };
289
+ }
290
+
291
+ function createEmptyUsageStats(): UsageStats {
292
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
293
+ }
294
+
295
+ function applyAssistantUsage(result: SingleResult, message: Message): void {
296
+ if (message.role !== "assistant") {
297
+ return;
298
+ }
299
+ result.usage.turns++;
300
+ const usage = message.usage;
301
+ if (!usage) {
302
+ return;
303
+ }
304
+ result.usage.input += usage.input || 0;
305
+ result.usage.output += usage.output || 0;
306
+ result.usage.cacheRead += usage.cacheRead || 0;
307
+ result.usage.cacheWrite += usage.cacheWrite || 0;
308
+ result.usage.cost += usage.cost?.total || 0;
309
+ result.usage.contextTokens = usage.totalTokens || 0;
310
+ if (!result.model && message.model) result.model = message.model;
311
+ if (message.stopReason) result.stopReason = message.stopReason;
312
+ if (message.errorMessage) result.errorMessage = message.errorMessage;
313
+ }
314
+
315
+ function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
316
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${APP_NAME}-subagent-`));
317
+ const safeName = agentName.replace(/[^\w.-]+/gu, "_");
318
+ const filePath = path.join(tempDir, `prompt-${safeName}.md`);
319
+ fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
320
+ return { dir: tempDir, filePath };
321
+ }
322
+
323
+ function parseJsonLines(stream: NodeJS.ReadableStream, onLine: (line: string) => void): void {
324
+ let buffer = "";
325
+ stream.on("data", (data) => {
326
+ buffer += data.toString();
327
+ const lines = buffer.split(/\r?\n/u);
328
+ buffer = lines.pop() || "";
329
+ for (const line of lines) onLine(line);
330
+ });
331
+ stream.on("end", () => {
332
+ if (buffer.trim().length > 0) {
333
+ onLine(buffer);
334
+ }
335
+ });
336
+ }
337
+
338
+ function createDefaultSubagentRunner(): SubagentRunner {
339
+ return ({ invocation, onMessage, signal }) =>
340
+ new Promise<SubagentRunResult>((resolve) => {
341
+ const child = spawn(invocation.command, invocation.args, {
342
+ cwd: invocation.cwd,
343
+ shell: false,
344
+ stdio: ["ignore", "pipe", "pipe"],
345
+ windowsHide: true,
346
+ });
347
+ let stderr = "";
348
+ let launchError: string | undefined;
349
+ let settled = false;
350
+
351
+ const finish = (result: SubagentRunResult) => {
352
+ if (settled) return;
353
+ settled = true;
354
+ resolve(result);
355
+ };
356
+
357
+ const killChild = () => {
358
+ child.kill("SIGTERM");
359
+ setTimeout(() => {
360
+ if (!child.killed) child.kill("SIGKILL");
361
+ }, 5000);
362
+ };
363
+
364
+ if (signal) {
365
+ if (signal.aborted) {
366
+ killChild();
367
+ } else {
368
+ signal.addEventListener("abort", killChild, { once: true });
369
+ }
370
+ }
371
+
372
+ parseJsonLines(child.stdout, (line) => {
373
+ if (!line.trim()) return;
374
+ let event: unknown;
375
+ try {
376
+ event = JSON.parse(line);
377
+ } catch {
378
+ return;
379
+ }
380
+
381
+ if (typeof event !== "object" || event === null) return;
382
+ const typedEvent = event as { type?: string; message?: Message };
383
+ if ((typedEvent.type === "message_end" || typedEvent.type === "tool_result_end") && typedEvent.message) {
384
+ onMessage(typedEvent.message);
385
+ }
386
+ });
387
+
388
+ child.stderr.on("data", (data) => {
389
+ stderr += data.toString();
390
+ });
391
+
392
+ child.on("error", (error) => {
393
+ launchError = error.message;
394
+ });
395
+
396
+ child.on("close", (code) => {
397
+ if (signal) {
398
+ signal.removeEventListener("abort", killChild);
399
+ }
400
+ finish({
401
+ exitCode: code ?? 1,
402
+ stderr: trimOutput(stderr),
403
+ launchError,
404
+ });
405
+ });
406
+ });
407
+ }
408
+
190
409
  async function mapWithConcurrencyLimit<TIn, TOut>(
191
410
  items: TIn[],
192
411
  concurrency: number,
@@ -207,12 +426,30 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
207
426
  return results;
208
427
  }
209
428
 
210
- function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
211
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
212
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
213
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
214
- fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
215
- return { dir: tmpDir, filePath };
429
+ function formatDiscoveryError(error: AgentDiscoveryError): string {
430
+ return `Agent discovery failed at ${error.path}: ${error.reason}`;
431
+ }
432
+
433
+ function getUnknownAgentDiagnostic(agentName: string, agents: AgentConfig[]): string {
434
+ const available = agents.map((agent) => `"${agent.name}" (${agent.source})`).join(", ") || "none";
435
+ return `Unknown agent: "${agentName}". Available agents: ${available}.`;
436
+ }
437
+
438
+ function getFailureDiagnostic(result: SingleResult): string {
439
+ if (result.diagnosticMessage) {
440
+ return result.diagnosticMessage;
441
+ }
442
+ if (result.errorMessage) {
443
+ return result.errorMessage;
444
+ }
445
+ if (result.stderr.trim().length > 0) {
446
+ return result.stderr.trim();
447
+ }
448
+ const output = extractFinalOutput(result.messages);
449
+ if (output.text.length > 0) {
450
+ return output.text;
451
+ }
452
+ return output.reason ? `Empty output: ${output.reason}` : "Empty output from child session.";
216
453
  }
217
454
 
218
455
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
@@ -220,6 +457,7 @@ type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
220
457
  async function runSingleAgent(
221
458
  defaultCwd: string,
222
459
  agents: AgentConfig[],
460
+ discoveryErrors: AgentDiscoveryError[],
223
461
  agentName: string,
224
462
  task: string,
225
463
  cwd: string | undefined,
@@ -227,30 +465,28 @@ async function runSingleAgent(
227
465
  signal: AbortSignal | undefined,
228
466
  onUpdate: OnUpdateCallback | undefined,
229
467
  makeDetails: (results: SingleResult[]) => SubagentDetails,
468
+ runSubagent: SubagentRunner,
230
469
  ): Promise<SingleResult> {
231
- const agent = agents.find((a) => a.name === agentName);
470
+ const agent = agents.find((candidate) => candidate.name === agentName);
232
471
 
233
472
  if (!agent) {
234
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
473
+ const discoveryError = findDiscoveryErrorForAgent(discoveryErrors, agentName);
235
474
  return {
236
475
  agent: agentName,
237
- agentSource: "unknown",
476
+ agentSource: discoveryError?.source ?? "unknown",
238
477
  task,
239
478
  exitCode: 1,
240
479
  messages: [],
241
- stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
242
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
480
+ stderr: "",
481
+ usage: createEmptyUsageStats(),
243
482
  step,
483
+ failureStage: discoveryError ? "discovery" : "lookup",
484
+ diagnosticMessage: discoveryError
485
+ ? formatDiscoveryError(discoveryError)
486
+ : getUnknownAgentDiagnostic(agentName, agents),
244
487
  };
245
488
  }
246
489
 
247
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
248
- if (agent.model) args.push("--model", agent.model);
249
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
250
-
251
- let tmpPromptDir: string | null = null;
252
- let tmpPromptPath: string | null = null;
253
-
254
490
  const currentResult: SingleResult = {
255
491
  agent: agentName,
256
492
  agentSource: agent.source,
@@ -258,121 +494,91 @@ async function runSingleAgent(
258
494
  exitCode: 0,
259
495
  messages: [],
260
496
  stderr: "",
261
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
497
+ usage: createEmptyUsageStats(),
262
498
  model: agent.model,
263
499
  step,
264
500
  };
265
501
 
266
502
  const emitUpdate = () => {
267
- if (onUpdate) {
268
- onUpdate({
269
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
270
- details: makeDetails([currentResult]),
271
- });
272
- }
503
+ if (!onUpdate) return;
504
+ const output = extractFinalOutput(currentResult.messages);
505
+ onUpdate({
506
+ content: [{ type: "text", text: output.text || "(running...)" }],
507
+ details: makeDetails([currentResult]),
508
+ });
273
509
  };
274
510
 
511
+ let tempPromptDir: string | null = null;
512
+ let tempPromptPath: string | null = null;
513
+
275
514
  try {
276
- if (agent.systemPrompt.trim()) {
277
- const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
278
- tmpPromptDir = tmp.dir;
279
- tmpPromptPath = tmp.filePath;
280
- args.push("--append-system-prompt", tmpPromptPath);
515
+ const invocation = buildSubagentInvocation(defaultCwd, agent, task, cwd);
516
+ currentResult.invocation = invocation;
517
+
518
+ if (agent.systemPrompt.trim().length > 0) {
519
+ const tempPrompt = writePromptToTempFile(agent.name, agent.systemPrompt);
520
+ tempPromptDir = tempPrompt.dir;
521
+ tempPromptPath = tempPrompt.filePath;
522
+ invocation.args.splice(invocation.args.length - 1, 0, "--append-system-prompt", tempPrompt.filePath);
523
+ invocation.displayCommand = [invocation.command, ...invocation.args].join(" ");
281
524
  }
282
525
 
283
- args.push(`Task: ${task}`);
284
- let wasAborted = false;
285
-
286
- const exitCode = await new Promise<number>((resolve) => {
287
- const proc = spawn("pi", args, { cwd: cwd ?? defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
288
- let buffer = "";
289
-
290
- const processLine = (line: string) => {
291
- if (!line.trim()) return;
292
- let event: any;
293
- try {
294
- event = JSON.parse(line);
295
- } catch {
296
- return;
297
- }
298
-
299
- if (event.type === "message_end" && event.message) {
300
- const msg = event.message as Message;
301
- currentResult.messages.push(msg);
302
-
303
- if (msg.role === "assistant") {
304
- currentResult.usage.turns++;
305
- const usage = msg.usage;
306
- if (usage) {
307
- currentResult.usage.input += usage.input || 0;
308
- currentResult.usage.output += usage.output || 0;
309
- currentResult.usage.cacheRead += usage.cacheRead || 0;
310
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
311
- currentResult.usage.cost += usage.cost?.total || 0;
312
- currentResult.usage.contextTokens = usage.totalTokens || 0;
313
- }
314
- if (!currentResult.model && msg.model) currentResult.model = msg.model;
315
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
316
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
317
- }
318
- emitUpdate();
319
- }
320
-
321
- if (event.type === "tool_result_end" && event.message) {
322
- currentResult.messages.push(event.message as Message);
323
- emitUpdate();
324
- }
325
- };
526
+ const runResult = await runSubagent({
527
+ invocation,
528
+ agent,
529
+ signal,
530
+ onMessage: (message) => {
531
+ currentResult.messages.push(message);
532
+ applyAssistantUsage(currentResult, message);
533
+ emitUpdate();
534
+ },
535
+ });
326
536
 
327
- proc.stdout.on("data", (data) => {
328
- buffer += data.toString();
329
- const lines = buffer.split("\n");
330
- buffer = lines.pop() || "";
331
- for (const line of lines) processLine(line);
332
- });
537
+ currentResult.exitCode = runResult.exitCode;
538
+ currentResult.stderr = runResult.stderr;
333
539
 
334
- proc.stderr.on("data", (data) => {
335
- currentResult.stderr += data.toString();
336
- });
540
+ if (runResult.launchError) {
541
+ currentResult.failureStage = "launch";
542
+ currentResult.diagnosticMessage = `Failed to launch child process: ${runResult.launchError}. Command: ${invocation.displayCommand}`;
543
+ }
337
544
 
338
- proc.on("close", (code) => {
339
- if (buffer.trim()) processLine(buffer);
340
- resolve(code ?? 0);
341
- });
545
+ if (currentResult.stopReason === "error" || currentResult.errorMessage) {
546
+ currentResult.failureStage = "provider";
547
+ if (!currentResult.diagnosticMessage) {
548
+ currentResult.diagnosticMessage = currentResult.errorMessage;
549
+ }
550
+ }
342
551
 
343
- proc.on("error", () => {
344
- resolve(1);
345
- });
552
+ if (!currentResult.failureStage && currentResult.exitCode !== 0) {
553
+ currentResult.failureStage = "result";
554
+ currentResult.diagnosticMessage =
555
+ currentResult.stderr.trim().length > 0
556
+ ? currentResult.stderr.trim()
557
+ : `Child process exited with code ${currentResult.exitCode}. Command: ${invocation.displayCommand}`;
558
+ }
346
559
 
347
- if (signal) {
348
- const killProc = () => {
349
- wasAborted = true;
350
- proc.kill("SIGTERM");
351
- setTimeout(() => {
352
- if (!proc.killed) proc.kill("SIGKILL");
353
- }, 5000);
354
- };
355
- if (signal.aborted) killProc();
356
- else signal.addEventListener("abort", killProc, { once: true });
357
- }
358
- });
560
+ const output = extractFinalOutput(currentResult.messages);
561
+ if (!currentResult.failureStage && output.text.length === 0) {
562
+ currentResult.failureStage = "result";
563
+ currentResult.diagnosticMessage = `Final assistant output was empty: ${output.reason ?? "unknown reason"}.`;
564
+ }
359
565
 
360
- currentResult.exitCode = exitCode;
361
- if (wasAborted) throw new Error("Subagent was aborted");
362
566
  return currentResult;
363
567
  } finally {
364
- if (tmpPromptPath)
568
+ if (tempPromptPath) {
365
569
  try {
366
- fs.unlinkSync(tmpPromptPath);
570
+ fs.unlinkSync(tempPromptPath);
367
571
  } catch {
368
572
  /* ignore */
369
573
  }
370
- if (tmpPromptDir)
574
+ }
575
+ if (tempPromptDir) {
371
576
  try {
372
- fs.rmdirSync(tmpPromptDir);
577
+ fs.rmdirSync(tempPromptDir);
373
578
  } catch {
374
579
  /* ignore */
375
580
  }
581
+ }
376
582
  }
377
583
  }
378
584
 
@@ -405,19 +611,48 @@ const SubagentParams = Type.Object({
405
611
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
406
612
  });
407
613
 
408
- export default function (pi: ExtensionAPI) {
409
- pi.registerTool({
614
+ function createDetailsFactory(
615
+ mode: "single" | "parallel" | "chain",
616
+ agentScope: AgentScope,
617
+ projectAgentsDir: string | null,
618
+ discoveryErrors: AgentDiscoveryError[],
619
+ ): (results: SingleResult[]) => SubagentDetails {
620
+ return (results) => ({
621
+ mode,
622
+ agentScope,
623
+ projectAgentsDir,
624
+ discoveryErrors,
625
+ results,
626
+ });
627
+ }
628
+
629
+ function hasToolErrorFlag(result: AgentToolResult<SubagentDetails>): boolean {
630
+ return (result as AgentToolResult<SubagentDetails> & { isError?: boolean }).isError === true;
631
+ }
632
+
633
+ export function createSubagentTool(options?: {
634
+ runSubagent?: SubagentRunner;
635
+ }): ToolDefinition<typeof SubagentParams, SubagentDetails> {
636
+ const runSubagent = options?.runSubagent ?? createDefaultSubagentRunner();
637
+
638
+ return {
410
639
  name: "subagent",
411
640
  label: "Subagent",
412
641
  description: [
413
642
  "Delegate tasks to specialized subagents with isolated context.",
414
643
  "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
415
- 'Default agent scope is "user" (from ~/.pi/agent/agents).',
416
- 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
644
+ 'Default agent scope is "user" (from ~/.jensen/agent/agents).',
645
+ 'To enable project-local agents in .jensen/agents, set agentScope: "both" (or "project").',
417
646
  ].join(" "),
418
647
  parameters: SubagentParams,
419
648
 
420
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
649
+ async execute(
650
+ _toolCallId: string,
651
+ params: SubagentParamsType,
652
+ signal: AbortSignal | undefined,
653
+ onUpdate: OnUpdateCallback | undefined,
654
+ ctx: ExtensionContext,
655
+ ) {
421
656
  const agentScope: AgentScope = params.agentScope ?? "user";
422
657
  const discovery = discoverAgents(ctx.cwd, agentScope);
423
658
  const agents = discovery.agents;
@@ -428,225 +663,226 @@ export default function (pi: ExtensionAPI) {
428
663
  const hasSingle = Boolean(params.agent && params.task);
429
664
  const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
430
665
 
431
- const makeDetails =
432
- (mode: "single" | "parallel" | "chain") =>
433
- (results: SingleResult[]): SubagentDetails => ({
434
- mode,
435
- agentScope,
436
- projectAgentsDir: discovery.projectAgentsDir,
437
- results,
438
- });
439
-
440
666
  if (modeCount !== 1) {
441
- const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
667
+ const available = agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
442
668
  return {
443
- content: [
444
- {
445
- type: "text",
446
- text: `Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`,
447
- },
448
- ],
449
- details: makeDetails("single")([]),
669
+ content: textContent(`Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`),
670
+ details: createDetailsFactory("single", agentScope, discovery.projectAgentsDir, discovery.errors)([]),
450
671
  };
451
672
  }
452
673
 
453
674
  if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
454
675
  const requestedAgentNames = new Set<string>();
455
676
  if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
456
- if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
677
+ if (params.tasks) for (const task of params.tasks) requestedAgentNames.add(task.agent);
457
678
  if (params.agent) requestedAgentNames.add(params.agent);
458
679
 
459
680
  const projectAgentsRequested = Array.from(requestedAgentNames)
460
- .map((name) => agents.find((a) => a.name === name))
461
- .filter((a): a is AgentConfig => a?.source === "project");
681
+ .map((name) => agents.find((candidate) => candidate.name === name))
682
+ .filter((candidate): candidate is AgentConfig => candidate?.source === "project");
462
683
 
463
684
  if (projectAgentsRequested.length > 0) {
464
- const names = projectAgentsRequested.map((a) => a.name).join(", ");
685
+ const names = projectAgentsRequested.map((agent) => agent.name).join(", ");
465
686
  const dir = discovery.projectAgentsDir ?? "(unknown)";
466
- const ok = await ctx.ui.confirm(
687
+ const approved = await ctx.ui.confirm(
467
688
  "Run project-local agents?",
468
689
  `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
469
690
  );
470
- if (!ok)
691
+ if (!approved) {
471
692
  return {
472
- content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
473
- details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
693
+ content: textContent("Canceled: project-local agents not approved."),
694
+ details: createDetailsFactory(
695
+ hasChain ? "chain" : hasTasks ? "parallel" : "single",
696
+ agentScope,
697
+ discovery.projectAgentsDir,
698
+ discovery.errors,
699
+ )([]),
474
700
  };
701
+ }
475
702
  }
476
703
  }
477
704
 
478
705
  if (params.chain && params.chain.length > 0) {
706
+ const makeDetails = createDetailsFactory("chain", agentScope, discovery.projectAgentsDir, discovery.errors);
479
707
  const results: SingleResult[] = [];
480
708
  let previousOutput = "";
481
709
 
482
710
  for (let i = 0; i < params.chain.length; i++) {
483
711
  const step = params.chain[i];
484
- const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
485
-
486
- // Create update callback that includes all previous results
712
+ const taskWithContext = step.task.replace(/\{previous\}/gu, previousOutput);
487
713
  const chainUpdate: OnUpdateCallback | undefined = onUpdate
488
714
  ? (partial) => {
489
- // Combine completed results with current streaming result
490
715
  const currentResult = partial.details?.results[0];
491
716
  if (currentResult) {
492
- const allResults = [...results, currentResult];
493
- onUpdate({
494
- content: partial.content,
495
- details: makeDetails("chain")(allResults),
496
- });
717
+ onUpdate({ content: partial.content, details: makeDetails([...results, currentResult]) });
497
718
  }
498
719
  }
499
720
  : undefined;
500
-
501
721
  const result = await runSingleAgent(
502
722
  ctx.cwd,
503
723
  agents,
724
+ discovery.errors,
504
725
  step.agent,
505
726
  taskWithContext,
506
727
  step.cwd,
507
728
  i + 1,
508
729
  signal,
509
730
  chainUpdate,
510
- makeDetails("chain"),
731
+ makeDetails,
732
+ runSubagent,
511
733
  );
512
734
  results.push(result);
513
735
 
514
- const isError =
515
- result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
516
- if (isError) {
517
- const errorMsg =
518
- result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
736
+ if (
737
+ result.exitCode !== 0 ||
738
+ result.stopReason === "error" ||
739
+ result.stopReason === "aborted" ||
740
+ result.failureStage
741
+ ) {
519
742
  return {
520
- content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
521
- details: makeDetails("chain")(results),
743
+ content: textContent(
744
+ `Chain stopped at step ${i + 1} (${step.agent}): ${getFailureDiagnostic(result)}`,
745
+ ),
746
+ details: makeDetails(results),
522
747
  isError: true,
523
748
  };
524
749
  }
525
- previousOutput = getFinalOutput(result.messages);
750
+ previousOutput = extractFinalOutput(result.messages).text;
526
751
  }
752
+
527
753
  return {
528
- content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
529
- details: makeDetails("chain")(results),
754
+ content: textContent(extractFinalOutput(results[results.length - 1].messages).text),
755
+ details: makeDetails(results),
530
756
  };
531
757
  }
532
758
 
533
759
  if (params.tasks && params.tasks.length > 0) {
534
- if (params.tasks.length > MAX_PARALLEL_TASKS)
760
+ if (params.tasks.length > MAX_PARALLEL_TASKS) {
535
761
  return {
536
- content: [
537
- {
538
- type: "text",
539
- text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
540
- },
541
- ],
542
- details: makeDetails("parallel")([]),
543
- };
544
-
545
- // Track all results for streaming updates
546
- const allResults: SingleResult[] = new Array(params.tasks.length);
547
-
548
- // Initialize placeholder results
549
- for (let i = 0; i < params.tasks.length; i++) {
550
- allResults[i] = {
551
- agent: params.tasks[i].agent,
552
- agentSource: "unknown",
553
- task: params.tasks[i].task,
554
- exitCode: -1, // -1 = still running
555
- messages: [],
556
- stderr: "",
557
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
762
+ content: textContent(
763
+ `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
764
+ ),
765
+ details: createDetailsFactory(
766
+ "parallel",
767
+ agentScope,
768
+ discovery.projectAgentsDir,
769
+ discovery.errors,
770
+ )([]),
558
771
  };
559
772
  }
560
773
 
774
+ const makeDetails = createDetailsFactory(
775
+ "parallel",
776
+ agentScope,
777
+ discovery.projectAgentsDir,
778
+ discovery.errors,
779
+ );
780
+ const allResults: SingleResult[] = params.tasks.map((task) => ({
781
+ agent: task.agent,
782
+ agentSource: "unknown",
783
+ task: task.task,
784
+ exitCode: -1,
785
+ messages: [],
786
+ stderr: "",
787
+ usage: createEmptyUsageStats(),
788
+ }));
789
+
561
790
  const emitParallelUpdate = () => {
562
- if (onUpdate) {
563
- const running = allResults.filter((r) => r.exitCode === -1).length;
564
- const done = allResults.filter((r) => r.exitCode !== -1).length;
565
- onUpdate({
566
- content: [
567
- { type: "text", text: `Parallel: ${done}/${allResults.length} done, ${running} running...` },
568
- ],
569
- details: makeDetails("parallel")([...allResults]),
570
- });
571
- }
791
+ if (!onUpdate) return;
792
+ const running = allResults.filter((result) => result.exitCode === -1).length;
793
+ const done = allResults.length - running;
794
+ onUpdate({
795
+ content: textContent(`Parallel: ${done}/${allResults.length} done, ${running} running...`),
796
+ details: makeDetails([...allResults]),
797
+ });
572
798
  };
573
799
 
574
- const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
800
+ const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task, index) => {
575
801
  const result = await runSingleAgent(
576
802
  ctx.cwd,
577
803
  agents,
578
- t.agent,
579
- t.task,
580
- t.cwd,
804
+ discovery.errors,
805
+ task.agent,
806
+ task.task,
807
+ task.cwd,
581
808
  undefined,
582
809
  signal,
583
- // Per-task update callback
584
810
  (partial) => {
585
- if (partial.details?.results[0]) {
586
- allResults[index] = partial.details.results[0];
811
+ const currentResult = partial.details?.results[0];
812
+ if (currentResult) {
813
+ allResults[index] = currentResult;
587
814
  emitParallelUpdate();
588
815
  }
589
816
  },
590
- makeDetails("parallel"),
817
+ makeDetails,
818
+ runSubagent,
591
819
  );
592
820
  allResults[index] = result;
593
821
  emitParallelUpdate();
594
822
  return result;
595
823
  });
596
824
 
597
- const successCount = results.filter((r) => r.exitCode === 0).length;
598
- const summaries = results.map((r) => {
599
- const output = getFinalOutput(r.messages);
600
- const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
601
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
825
+ const successCount = results.filter((result) => result.exitCode === 0 && !result.failureStage).length;
826
+ const summaries = results.map((result) => {
827
+ const output = extractFinalOutput(result.messages).text;
828
+ const previewSource = output || getFailureDiagnostic(result);
829
+ const preview = previewSource.slice(0, 100) + (previewSource.length > 100 ? "..." : "");
830
+ return `[${result.agent}] ${result.exitCode === 0 && !result.failureStage ? "completed" : "failed"}: ${preview}`;
602
831
  });
603
832
  return {
604
- content: [
605
- {
606
- type: "text",
607
- text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
608
- },
609
- ],
610
- details: makeDetails("parallel")(results),
833
+ content: textContent(
834
+ `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
835
+ ),
836
+ details: makeDetails(results),
611
837
  };
612
838
  }
613
839
 
614
840
  if (params.agent && params.task) {
841
+ const makeDetails = createDetailsFactory(
842
+ "single",
843
+ agentScope,
844
+ discovery.projectAgentsDir,
845
+ discovery.errors,
846
+ );
615
847
  const result = await runSingleAgent(
616
848
  ctx.cwd,
617
849
  agents,
850
+ discovery.errors,
618
851
  params.agent,
619
852
  params.task,
620
853
  params.cwd,
621
854
  undefined,
622
855
  signal,
623
856
  onUpdate,
624
- makeDetails("single"),
857
+ makeDetails,
858
+ runSubagent,
625
859
  );
626
- const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
627
- if (isError) {
628
- const errorMsg =
629
- result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
860
+ if (
861
+ result.exitCode !== 0 ||
862
+ result.stopReason === "error" ||
863
+ result.stopReason === "aborted" ||
864
+ result.failureStage
865
+ ) {
630
866
  return {
631
- content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
632
- details: makeDetails("single")([result]),
867
+ content: textContent(getFailureDiagnostic(result)),
868
+ details: makeDetails([result]),
633
869
  isError: true,
634
870
  };
635
871
  }
636
872
  return {
637
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
638
- details: makeDetails("single")([result]),
873
+ content: textContent(extractFinalOutput(result.messages).text),
874
+ details: makeDetails([result]),
639
875
  };
640
876
  }
641
877
 
642
- const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
878
+ const available = agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
643
879
  return {
644
- content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
645
- details: makeDetails("single")([]),
880
+ content: textContent(`Invalid parameters. Available agents: ${available}`),
881
+ details: createDetailsFactory("single", agentScope, discovery.projectAgentsDir, discovery.errors)([]),
646
882
  };
647
883
  },
648
884
 
649
- renderCall(args, theme) {
885
+ renderCall(args: SubagentParamsType, theme: Theme) {
650
886
  const scope: AgentScope = args.agentScope ?? "user";
651
887
  if (args.chain && args.chain.length > 0) {
652
888
  let text =
@@ -655,15 +891,9 @@ export default function (pi: ExtensionAPI) {
655
891
  theme.fg("muted", ` [${scope}]`);
656
892
  for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
657
893
  const step = args.chain[i];
658
- // Clean up {previous} placeholder for display
659
- const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
894
+ const cleanTask = step.task.replace(/\{previous\}/gu, "").trim();
660
895
  const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
661
- text +=
662
- "\n " +
663
- theme.fg("muted", `${i + 1}.`) +
664
- " " +
665
- theme.fg("accent", step.agent) +
666
- theme.fg("dim", ` ${preview}`);
896
+ text += `\n ${theme.fg("muted", `${i + 1}.`)} ${theme.fg("accent", step.agent)}${theme.fg("dim", ` ${preview}`)}`;
667
897
  }
668
898
  if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
669
899
  return new Text(text, 0, 0);
@@ -673,24 +903,24 @@ export default function (pi: ExtensionAPI) {
673
903
  theme.fg("toolTitle", theme.bold("subagent ")) +
674
904
  theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
675
905
  theme.fg("muted", ` [${scope}]`);
676
- for (const t of args.tasks.slice(0, 3)) {
677
- const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
678
- text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
906
+ for (const task of args.tasks.slice(0, 3)) {
907
+ const preview = task.task.length > 40 ? `${task.task.slice(0, 40)}...` : task.task;
908
+ text += `\n ${theme.fg("accent", task.agent)}${theme.fg("dim", ` ${preview}`)}`;
679
909
  }
680
910
  if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
681
911
  return new Text(text, 0, 0);
682
912
  }
913
+
683
914
  const agentName = args.agent || "...";
684
915
  const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
685
- let text =
686
- theme.fg("toolTitle", theme.bold("subagent ")) +
687
- theme.fg("accent", agentName) +
688
- theme.fg("muted", ` [${scope}]`);
689
- text += `\n ${theme.fg("dim", preview)}`;
690
- return new Text(text, 0, 0);
916
+ return new Text(
917
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", agentName)}${theme.fg("muted", ` [${scope}]`)}\n ${theme.fg("dim", preview)}`,
918
+ 0,
919
+ 0,
920
+ );
691
921
  },
692
922
 
693
- renderResult(result, { expanded }, theme) {
923
+ renderResult(result: AgentToolResult<SubagentDetails>, { expanded }: ToolRenderResultOptions, theme: Theme) {
694
924
  const details = result.details as SubagentDetails | undefined;
695
925
  if (!details || details.results.length === 0) {
696
926
  const text = result.content[0];
@@ -698,7 +928,6 @@ export default function (pi: ExtensionAPI) {
698
928
  }
699
929
 
700
930
  const mdTheme = getMarkdownTheme();
701
-
702
931
  const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
703
932
  const toShow = limit ? items.slice(-limit) : items;
704
933
  const skipped = limit && items.length > limit ? items.length - limit : 0;
@@ -716,29 +945,31 @@ export default function (pi: ExtensionAPI) {
716
945
  };
717
946
 
718
947
  if (details.mode === "single" && details.results.length === 1) {
719
- const r = details.results[0];
720
- const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
948
+ const single = details.results[0];
949
+ const isError = hasToolErrorFlag(result) || single.exitCode !== 0 || !!single.failureStage;
721
950
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
722
- const displayItems = getDisplayItems(r.messages);
723
- const finalOutput = getFinalOutput(r.messages);
951
+ const displayItems = getDisplayItems(single.messages);
952
+ const finalOutput = extractFinalOutput(single.messages).text;
724
953
 
725
954
  if (expanded) {
726
955
  const container = new Container();
727
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
728
- if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
956
+ let header = `${icon} ${theme.fg("toolTitle", theme.bold(single.agent))}${theme.fg("muted", ` (${single.agentSource})`)}`;
957
+ if (single.failureStage) header += ` ${theme.fg("error", `[${single.failureStage}]`)}`;
729
958
  container.addChild(new Text(header, 0, 0));
730
- if (isError && r.errorMessage)
731
- container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
959
+ if (single.diagnosticMessage) {
960
+ container.addChild(new Spacer(1));
961
+ container.addChild(new Text(theme.fg("error", single.diagnosticMessage), 0, 0));
962
+ }
732
963
  container.addChild(new Spacer(1));
733
964
  container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
734
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
965
+ container.addChild(new Text(theme.fg("dim", single.task), 0, 0));
735
966
  container.addChild(new Spacer(1));
736
967
  container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
737
968
  if (displayItems.length === 0 && !finalOutput) {
738
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
969
+ container.addChild(new Text(theme.fg("muted", "(no assistant output)"), 0, 0));
739
970
  } else {
740
971
  for (const item of displayItems) {
741
- if (item.type === "toolCall")
972
+ if (item.type === "toolCall") {
742
973
  container.addChild(
743
974
  new Text(
744
975
  theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
@@ -746,219 +977,109 @@ export default function (pi: ExtensionAPI) {
746
977
  0,
747
978
  ),
748
979
  );
980
+ }
749
981
  }
750
982
  if (finalOutput) {
751
983
  container.addChild(new Spacer(1));
752
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
984
+ container.addChild(new Markdown(finalOutput, 0, 0, mdTheme));
753
985
  }
754
986
  }
755
- const usageStr = formatUsageStats(r.usage, r.model);
756
- if (usageStr) {
987
+ const usageText = formatUsageStats(single.usage, single.model);
988
+ if (usageText) {
757
989
  container.addChild(new Spacer(1));
758
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
990
+ container.addChild(new Text(theme.fg("dim", usageText), 0, 0));
759
991
  }
760
992
  return container;
761
993
  }
762
994
 
763
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
764
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
765
- if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
766
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
767
- else {
995
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold(single.agent))}${theme.fg("muted", ` (${single.agentSource})`)}`;
996
+ if (single.failureStage) text += ` ${theme.fg("error", `[${single.failureStage}]`)}`;
997
+ if (single.diagnosticMessage) {
998
+ text += `\n${theme.fg("error", single.diagnosticMessage)}`;
999
+ } else if (displayItems.length === 0) {
1000
+ text += `\n${theme.fg("muted", "(no assistant output)")}`;
1001
+ } else {
768
1002
  text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
769
- if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
770
1003
  }
771
- const usageStr = formatUsageStats(r.usage, r.model);
772
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
1004
+ const usageText = formatUsageStats(single.usage, single.model);
1005
+ if (usageText) text += `\n${theme.fg("dim", usageText)}`;
773
1006
  return new Text(text, 0, 0);
774
1007
  }
775
1008
 
776
1009
  const aggregateUsage = (results: SingleResult[]) => {
777
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
778
- for (const r of results) {
779
- total.input += r.usage.input;
780
- total.output += r.usage.output;
781
- total.cacheRead += r.usage.cacheRead;
782
- total.cacheWrite += r.usage.cacheWrite;
783
- total.cost += r.usage.cost;
784
- total.turns += r.usage.turns;
1010
+ const total = createEmptyUsageStats();
1011
+ for (const single of results) {
1012
+ total.input += single.usage.input;
1013
+ total.output += single.usage.output;
1014
+ total.cacheRead += single.usage.cacheRead;
1015
+ total.cacheWrite += single.usage.cacheWrite;
1016
+ total.cost += single.usage.cost;
1017
+ total.turns += single.usage.turns;
785
1018
  }
786
1019
  return total;
787
1020
  };
788
1021
 
789
1022
  if (details.mode === "chain") {
790
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
1023
+ const successCount = details.results.filter(
1024
+ (single) => single.exitCode === 0 && !single.failureStage,
1025
+ ).length;
791
1026
  const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
792
-
793
- if (expanded) {
794
- const container = new Container();
795
- container.addChild(
796
- new Text(
797
- icon +
798
- " " +
799
- theme.fg("toolTitle", theme.bold("chain ")) +
800
- theme.fg("accent", `${successCount}/${details.results.length} steps`),
801
- 0,
802
- 0,
803
- ),
804
- );
805
-
806
- for (const r of details.results) {
807
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
808
- const displayItems = getDisplayItems(r.messages);
809
- const finalOutput = getFinalOutput(r.messages);
810
-
811
- container.addChild(new Spacer(1));
812
- container.addChild(
813
- new Text(
814
- `${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
815
- 0,
816
- 0,
817
- ),
818
- );
819
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
820
-
821
- // Show tool calls
822
- for (const item of displayItems) {
823
- if (item.type === "toolCall") {
824
- container.addChild(
825
- new Text(
826
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
827
- 0,
828
- 0,
829
- ),
830
- );
831
- }
832
- }
833
-
834
- // Show final output as markdown
835
- if (finalOutput) {
836
- container.addChild(new Spacer(1));
837
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
838
- }
839
-
840
- const stepUsage = formatUsageStats(r.usage, r.model);
841
- if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
1027
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${details.results.length} steps`)}`;
1028
+ for (const single of details.results) {
1029
+ const stepIcon =
1030
+ single.exitCode === 0 && !single.failureStage ? theme.fg("success", "✓") : theme.fg("error", "✗");
1031
+ text += `\n\n${theme.fg("muted", `─── Step ${single.step}: `)}${theme.fg("accent", single.agent)} ${stepIcon}`;
1032
+ if (single.diagnosticMessage) {
1033
+ text += `\n${theme.fg("error", single.diagnosticMessage)}`;
1034
+ continue;
842
1035
  }
843
-
844
- const usageStr = formatUsageStats(aggregateUsage(details.results));
845
- if (usageStr) {
846
- container.addChild(new Spacer(1));
847
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
848
- }
849
- return container;
850
- }
851
-
852
- // Collapsed view
853
- let text =
854
- icon +
855
- " " +
856
- theme.fg("toolTitle", theme.bold("chain ")) +
857
- theme.fg("accent", `${successCount}/${details.results.length} steps`);
858
- for (const r of details.results) {
859
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
860
- const displayItems = getDisplayItems(r.messages);
861
- text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
862
- if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
863
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1036
+ const displayItems = getDisplayItems(single.messages);
1037
+ text += `\n${displayItems.length > 0 ? renderDisplayItems(displayItems, 5) : theme.fg("muted", "(no assistant output)")}`;
864
1038
  }
865
- const usageStr = formatUsageStats(aggregateUsage(details.results));
866
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
867
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1039
+ const usageText = formatUsageStats(aggregateUsage(details.results));
1040
+ if (usageText) text += `\n\n${theme.fg("dim", `Total: ${usageText}`)}`;
1041
+ if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
868
1042
  return new Text(text, 0, 0);
869
1043
  }
870
1044
 
871
- if (details.mode === "parallel") {
872
- const running = details.results.filter((r) => r.exitCode === -1).length;
873
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
874
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
875
- const isRunning = running > 0;
876
- const icon = isRunning
877
- ? theme.fg("warning", "⏳")
878
- : failCount > 0
879
- ? theme.fg("warning", "")
880
- : theme.fg("success", "✓");
881
- const status = isRunning
882
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
883
- : `${successCount}/${details.results.length} tasks`;
884
-
885
- if (expanded && !isRunning) {
886
- const container = new Container();
887
- container.addChild(
888
- new Text(
889
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
890
- 0,
891
- 0,
892
- ),
893
- );
894
-
895
- for (const r of details.results) {
896
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
897
- const displayItems = getDisplayItems(r.messages);
898
- const finalOutput = getFinalOutput(r.messages);
899
-
900
- container.addChild(new Spacer(1));
901
- container.addChild(
902
- new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
903
- );
904
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
905
-
906
- // Show tool calls
907
- for (const item of displayItems) {
908
- if (item.type === "toolCall") {
909
- container.addChild(
910
- new Text(
911
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
912
- 0,
913
- 0,
914
- ),
915
- );
916
- }
917
- }
918
-
919
- // Show final output as markdown
920
- if (finalOutput) {
921
- container.addChild(new Spacer(1));
922
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
923
- }
924
-
925
- const taskUsage = formatUsageStats(r.usage, r.model);
926
- if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
927
- }
928
-
929
- const usageStr = formatUsageStats(aggregateUsage(details.results));
930
- if (usageStr) {
931
- container.addChild(new Spacer(1));
932
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
933
- }
934
- return container;
935
- }
936
-
937
- // Collapsed view (or still running)
938
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
939
- for (const r of details.results) {
940
- const rIcon =
941
- r.exitCode === -1
942
- ? theme.fg("warning", "⏳")
943
- : r.exitCode === 0
944
- ? theme.fg("success", "✓")
945
- : theme.fg("error", "✗");
946
- const displayItems = getDisplayItems(r.messages);
947
- text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
948
- if (displayItems.length === 0)
949
- text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
950
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1045
+ const running = details.results.filter((single) => single.exitCode === -1).length;
1046
+ const successCount = details.results.filter((single) => single.exitCode === 0 && !single.failureStage).length;
1047
+ const failCount = details.results.filter((single) => single.exitCode > 0 || single.failureStage).length;
1048
+ const isRunning = running > 0;
1049
+ const icon = isRunning
1050
+ ? theme.fg("warning", "⏳")
1051
+ : failCount > 0
1052
+ ? theme.fg("warning", "◐")
1053
+ : theme.fg("success", "");
1054
+ const status = isRunning
1055
+ ? `${successCount + failCount}/${details.results.length} done, ${running} running`
1056
+ : `${successCount}/${details.results.length} tasks`;
1057
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
1058
+ for (const single of details.results) {
1059
+ const stepIcon =
1060
+ single.exitCode === -1
1061
+ ? theme.fg("warning", "⏳")
1062
+ : single.exitCode === 0 && !single.failureStage
1063
+ ? theme.fg("success", "")
1064
+ : theme.fg("error", "✗");
1065
+ text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", single.agent)} ${stepIcon}`;
1066
+ if (single.diagnosticMessage) {
1067
+ text += `\n${theme.fg("error", single.diagnosticMessage)}`;
1068
+ continue;
951
1069
  }
952
- if (!isRunning) {
953
- const usageStr = formatUsageStats(aggregateUsage(details.results));
954
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
955
- }
956
- if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
957
- return new Text(text, 0, 0);
1070
+ const displayItems = getDisplayItems(single.messages);
1071
+ text += `\n${displayItems.length > 0 ? renderDisplayItems(displayItems, 5) : theme.fg("muted", isRunning ? "(running...)" : "(no assistant output)")}`;
958
1072
  }
959
-
960
- const text = result.content[0];
961
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1073
+ if (!isRunning) {
1074
+ const usageText = formatUsageStats(aggregateUsage(details.results));
1075
+ if (usageText) text += `\n\n${theme.fg("dim", `Total: ${usageText}`)}`;
1076
+ }
1077
+ if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1078
+ return new Text(text, 0, 0);
962
1079
  },
963
- });
1080
+ };
1081
+ }
1082
+
1083
+ export default function (pi: ExtensionAPI) {
1084
+ pi.registerTool(createSubagentTool());
964
1085
  }