@apholdings/jensen-code 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +6 -6
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -25
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +25 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +1 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +4 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +25 -11
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +5 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +0 -2
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -146
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/header.d.ts +9 -3
- package/dist/modes/interactive/components/header.d.ts.map +1 -1
- package/dist/modes/interactive/components/header.js +125 -196
- package/dist/modes/interactive/components/header.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +1 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +23 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +657 -243
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +2 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +8 -4
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/osgrep.ts +643 -0
- package/examples/extensions/subagent/agents.ts +150 -38
- package/examples/extensions/subagent/index.ts +634 -514
- package/package.json +4 -3
- package/examples/README.md +0 -25
- package/examples/extensions/README.md +0 -206
- package/examples/extensions/antigravity-image-gen.ts +0 -416
- package/examples/extensions/auto-commit-on-exit.ts +0 -50
- package/examples/extensions/bash-spawn-hook.ts +0 -31
- package/examples/extensions/bookmark.ts +0 -51
- package/examples/extensions/built-in-tool-renderer.ts +0 -247
- package/examples/extensions/claude-rules.ts +0 -87
- package/examples/extensions/commands.ts +0 -73
- package/examples/extensions/confirm-destructive.ts +0 -60
- package/examples/extensions/custom-compaction.ts +0 -115
- package/examples/extensions/custom-footer.ts +0 -65
- package/examples/extensions/custom-header.ts +0 -74
- package/examples/extensions/custom-provider-anthropic/index.ts +0 -605
- package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
- package/examples/extensions/custom-provider-anthropic/package.json +0 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -350
- package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
- package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -346
- package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
- package/examples/extensions/dirty-repo-guard.ts +0 -57
- package/examples/extensions/doom-overlay/README.md +0 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +0 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
- package/examples/extensions/doom-overlay/doom-component.ts +0 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
- package/examples/extensions/doom-overlay/index.ts +0 -75
- package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
- package/examples/extensions/dynamic-resources/SKILL.md +0 -8
- package/examples/extensions/dynamic-resources/dynamic.json +0 -79
- package/examples/extensions/dynamic-resources/dynamic.md +0 -5
- package/examples/extensions/dynamic-resources/index.ts +0 -16
- package/examples/extensions/dynamic-tools.ts +0 -75
- package/examples/extensions/event-bus.ts +0 -44
- package/examples/extensions/file-trigger.ts +0 -42
- package/examples/extensions/git-checkpoint.ts +0 -54
- package/examples/extensions/handoff.ts +0 -151
- package/examples/extensions/hello.ts +0 -26
- package/examples/extensions/inline-bash.ts +0 -95
- package/examples/extensions/input-transform.ts +0 -44
- package/examples/extensions/interactive-shell.ts +0 -197
- package/examples/extensions/mac-system-theme.ts +0 -48
- package/examples/extensions/message-renderer.ts +0 -60
- package/examples/extensions/minimal-mode.ts +0 -427
- package/examples/extensions/modal-editor.ts +0 -86
- package/examples/extensions/model-status.ts +0 -32
- package/examples/extensions/notify.ts +0 -56
- package/examples/extensions/overlay-qa-tests.ts +0 -1349
- package/examples/extensions/overlay-test.ts +0 -151
- package/examples/extensions/permission-gate.ts +0 -35
- package/examples/extensions/pirate.ts +0 -48
- package/examples/extensions/plan-mode/README.md +0 -65
- package/examples/extensions/plan-mode/index.ts +0 -341
- package/examples/extensions/plan-mode/utils.ts +0 -168
- package/examples/extensions/preset.ts +0 -399
- package/examples/extensions/protected-paths.ts +0 -31
- package/examples/extensions/provider-payload.ts +0 -15
- package/examples/extensions/qna.ts +0 -120
- package/examples/extensions/question.ts +0 -265
- package/examples/extensions/questionnaire.ts +0 -428
- package/examples/extensions/rainbow-editor.ts +0 -89
- package/examples/extensions/reload-runtime.ts +0 -38
- package/examples/extensions/rpc-demo.ts +0 -125
- package/examples/extensions/sandbox/index.ts +0 -319
- package/examples/extensions/sandbox/package-lock.json +0 -92
- package/examples/extensions/sandbox/package.json +0 -19
- package/examples/extensions/send-user-message.ts +0 -98
- package/examples/extensions/session-name.ts +0 -28
- package/examples/extensions/shutdown-command.ts +0 -64
- package/examples/extensions/snake.ts +0 -344
- package/examples/extensions/space-invaders.ts +0 -561
- package/examples/extensions/ssh.ts +0 -221
- package/examples/extensions/status-line.ts +0 -41
- package/examples/extensions/subagent/README.md +0 -172
- package/examples/extensions/subagent/agents/planner.md +0 -37
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/examples/extensions/subagent/agents/scout.md +0 -50
- package/examples/extensions/subagent/agents/worker.md +0 -24
- package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/examples/extensions/subagent/prompts/implement.md +0 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/examples/extensions/summarize.ts +0 -196
- package/examples/extensions/system-prompt-header.ts +0 -18
- package/examples/extensions/timed-confirm.ts +0 -71
- package/examples/extensions/titlebar-spinner.ts +0 -59
- package/examples/extensions/todo.ts +0 -300
- package/examples/extensions/tool-override.ts +0 -144
- package/examples/extensions/tools.ts +0 -147
- package/examples/extensions/trigger-compact.ts +0 -41
- package/examples/extensions/truncated-tool.ts +0 -193
- package/examples/extensions/widget-placement.ts +0 -18
- package/examples/extensions/with-deps/index.ts +0 -33
- package/examples/extensions/with-deps/package-lock.json +0 -31
- package/examples/extensions/with-deps/package.json +0 -22
- package/examples/rpc-extension-ui.ts +0 -632
- package/examples/sdk/01-minimal.ts +0 -23
- package/examples/sdk/02-custom-model.ts +0 -50
- package/examples/sdk/03-custom-prompt.ts +0 -56
- package/examples/sdk/04-skills.ts +0 -47
- package/examples/sdk/05-tools.ts +0 -57
- package/examples/sdk/06-extensions.ts +0 -89
- package/examples/sdk/07-context-files.ts +0 -41
- package/examples/sdk/08-prompt-templates.ts +0 -48
- package/examples/sdk/09-api-keys-and-oauth.ts +0 -49
- package/examples/sdk/10-settings.ts +0 -52
- package/examples/sdk/11-sessions.ts +0 -49
- package/examples/sdk/12-full-control.ts +0 -83
- 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 {
|
|
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 {
|
|
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
|
|
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 = (
|
|
132
|
+
const shortenPath = (filePath: string) => {
|
|
70
133
|
const home = os.homedir();
|
|
71
|
-
return
|
|
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
|
|
126
|
-
const preview =
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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((
|
|
470
|
+
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
232
471
|
|
|
233
472
|
if (!agent) {
|
|
234
|
-
const
|
|
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:
|
|
242
|
-
usage:
|
|
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:
|
|
497
|
+
usage: createEmptyUsageStats(),
|
|
262
498
|
model: agent.model,
|
|
263
499
|
step,
|
|
264
500
|
};
|
|
265
501
|
|
|
266
502
|
const emitUpdate = () => {
|
|
267
|
-
if (onUpdate)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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 (
|
|
568
|
+
if (tempPromptPath) {
|
|
365
569
|
try {
|
|
366
|
-
fs.unlinkSync(
|
|
570
|
+
fs.unlinkSync(tempPromptPath);
|
|
367
571
|
} catch {
|
|
368
572
|
/* ignore */
|
|
369
573
|
}
|
|
370
|
-
|
|
574
|
+
}
|
|
575
|
+
if (tempPromptDir) {
|
|
371
576
|
try {
|
|
372
|
-
fs.rmdirSync(
|
|
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
|
-
|
|
409
|
-
|
|
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 ~/.
|
|
416
|
-
'To enable project-local agents in .
|
|
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(
|
|
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((
|
|
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
|
|
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((
|
|
461
|
-
.filter((
|
|
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((
|
|
685
|
+
const names = projectAgentsRequested.map((agent) => agent.name).join(", ");
|
|
465
686
|
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
466
|
-
const
|
|
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 (!
|
|
691
|
+
if (!approved) {
|
|
471
692
|
return {
|
|
472
|
-
content:
|
|
473
|
-
details:
|
|
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\}/
|
|
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
|
-
|
|
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
|
|
731
|
+
makeDetails,
|
|
732
|
+
runSubagent,
|
|
511
733
|
);
|
|
512
734
|
results.push(result);
|
|
513
735
|
|
|
514
|
-
|
|
515
|
-
result.exitCode !== 0 ||
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
736
|
+
if (
|
|
737
|
+
result.exitCode !== 0 ||
|
|
738
|
+
result.stopReason === "error" ||
|
|
739
|
+
result.stopReason === "aborted" ||
|
|
740
|
+
result.failureStage
|
|
741
|
+
) {
|
|
519
742
|
return {
|
|
520
|
-
content:
|
|
521
|
-
|
|
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 =
|
|
750
|
+
previousOutput = extractFinalOutput(result.messages).text;
|
|
526
751
|
}
|
|
752
|
+
|
|
527
753
|
return {
|
|
528
|
-
content:
|
|
529
|
-
details: makeDetails(
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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 (
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
586
|
-
|
|
811
|
+
const currentResult = partial.details?.results[0];
|
|
812
|
+
if (currentResult) {
|
|
813
|
+
allResults[index] = currentResult;
|
|
587
814
|
emitParallelUpdate();
|
|
588
815
|
}
|
|
589
816
|
},
|
|
590
|
-
makeDetails
|
|
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((
|
|
598
|
-
const summaries = results.map((
|
|
599
|
-
const output =
|
|
600
|
-
const
|
|
601
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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
|
|
857
|
+
makeDetails,
|
|
858
|
+
runSubagent,
|
|
625
859
|
);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
860
|
+
if (
|
|
861
|
+
result.exitCode !== 0 ||
|
|
862
|
+
result.stopReason === "error" ||
|
|
863
|
+
result.stopReason === "aborted" ||
|
|
864
|
+
result.failureStage
|
|
865
|
+
) {
|
|
630
866
|
return {
|
|
631
|
-
content:
|
|
632
|
-
details: makeDetails(
|
|
867
|
+
content: textContent(getFailureDiagnostic(result)),
|
|
868
|
+
details: makeDetails([result]),
|
|
633
869
|
isError: true,
|
|
634
870
|
};
|
|
635
871
|
}
|
|
636
872
|
return {
|
|
637
|
-
content:
|
|
638
|
-
details: makeDetails(
|
|
873
|
+
content: textContent(extractFinalOutput(result.messages).text),
|
|
874
|
+
details: makeDetails([result]),
|
|
639
875
|
};
|
|
640
876
|
}
|
|
641
877
|
|
|
642
|
-
const available = agents.map((
|
|
878
|
+
const available = agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
|
|
643
879
|
return {
|
|
644
|
-
content:
|
|
645
|
-
details:
|
|
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
|
-
|
|
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
|
|
677
|
-
const preview =
|
|
678
|
-
text += `\n ${theme.fg("accent",
|
|
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
|
-
|
|
686
|
-
theme.fg("toolTitle", theme.bold("subagent "))
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|
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
|
|
720
|
-
const isError =
|
|
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(
|
|
723
|
-
const finalOutput =
|
|
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(
|
|
728
|
-
if (
|
|
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 (
|
|
731
|
-
container.addChild(new
|
|
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",
|
|
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,220 +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
|
|
984
|
+
container.addChild(new Markdown(finalOutput, 0, 0, mdTheme));
|
|
753
985
|
}
|
|
754
986
|
}
|
|
755
|
-
const
|
|
756
|
-
if (
|
|
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",
|
|
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(
|
|
764
|
-
if (
|
|
765
|
-
if (
|
|
766
|
-
|
|
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
|
|
772
|
-
if (
|
|
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 =
|
|
778
|
-
for (const
|
|
779
|
-
total.input +=
|
|
780
|
-
total.output +=
|
|
781
|
-
total.cacheRead +=
|
|
782
|
-
total.cacheWrite +=
|
|
783
|
-
total.cost +=
|
|
784
|
-
total.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(
|
|
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
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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
|
|
866
|
-
if (
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
953
|
-
|
|
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
|
-
|
|
961
|
-
|
|
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
|
}
|
|
965
|
-
|