@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -21,7 +21,6 @@ import { formatDuration } from "../render-utils";
|
|
|
21
21
|
import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
|
|
22
22
|
import { discoverAgents, getAgent } from "./discovery";
|
|
23
23
|
import { runSubprocess } from "./executor";
|
|
24
|
-
import { generateTaskName } from "./name-generator";
|
|
25
24
|
import { mapWithConcurrencyLimit } from "./parallel";
|
|
26
25
|
import { renderCall, renderResult } from "./render";
|
|
27
26
|
import {
|
|
@@ -135,17 +134,35 @@ export async function createTaskTool(
|
|
|
135
134
|
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
136
135
|
const startTime = Date.now();
|
|
137
136
|
const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
|
|
138
|
-
const context = params
|
|
139
|
-
const
|
|
137
|
+
const { agent: agentName, context, model, output: outputSchema } = params;
|
|
138
|
+
const modelOverride = model ?? session.getModelString?.();
|
|
139
|
+
|
|
140
|
+
// Validate agent exists
|
|
141
|
+
const agent = getAgent(agents, agentName);
|
|
142
|
+
if (!agent) {
|
|
143
|
+
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: `Unknown agent "${agentName}". Available: ${available}`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
details: {
|
|
152
|
+
projectAgentsDir,
|
|
153
|
+
results: [],
|
|
154
|
+
totalDurationMs: 0,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
140
158
|
|
|
141
159
|
// Handle empty or missing tasks
|
|
142
160
|
if (!params.tasks || params.tasks.length === 0) {
|
|
143
|
-
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
144
161
|
return {
|
|
145
162
|
content: [
|
|
146
163
|
{
|
|
147
164
|
type: "text",
|
|
148
|
-
text: `No tasks provided. Use: { tasks: [{
|
|
165
|
+
text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
|
|
149
166
|
},
|
|
150
167
|
],
|
|
151
168
|
details: {
|
|
@@ -173,6 +190,56 @@ export async function createTaskTool(
|
|
|
173
190
|
};
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
const tasks = params.tasks;
|
|
194
|
+
const missingTaskIndexes: number[] = [];
|
|
195
|
+
const idIndexes = new Map<string, number[]>();
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
198
|
+
const id = tasks[i]?.id;
|
|
199
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
200
|
+
missingTaskIndexes.push(i);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const normalizedId = id.toLowerCase();
|
|
204
|
+
const indexes = idIndexes.get(normalizedId);
|
|
205
|
+
if (indexes) {
|
|
206
|
+
indexes.push(i);
|
|
207
|
+
} else {
|
|
208
|
+
idIndexes.set(normalizedId, [i]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
|
|
213
|
+
for (const [normalizedId, indexes] of idIndexes.entries()) {
|
|
214
|
+
if (indexes.length > 1) {
|
|
215
|
+
duplicateIds.push({
|
|
216
|
+
id: tasks[indexes[0]]?.id ?? normalizedId,
|
|
217
|
+
indexes,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
|
|
223
|
+
const problems: string[] = [];
|
|
224
|
+
if (missingTaskIndexes.length > 0) {
|
|
225
|
+
problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
if (duplicateIds.length > 0) {
|
|
228
|
+
const details = duplicateIds
|
|
229
|
+
.map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`)
|
|
230
|
+
.join("; ");
|
|
231
|
+
problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
|
|
235
|
+
details: {
|
|
236
|
+
projectAgentsDir,
|
|
237
|
+
results: [],
|
|
238
|
+
totalDurationMs: 0,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
176
243
|
// Derive artifacts directory
|
|
177
244
|
const sessionFile = session.getSessionFile();
|
|
178
245
|
const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
|
|
@@ -197,88 +264,59 @@ export async function createTaskTool(
|
|
|
197
264
|
};
|
|
198
265
|
|
|
199
266
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
skippedSelfRecursion = blockedTasks.length;
|
|
208
|
-
|
|
209
|
-
if (skippedSelfRecursion > 0 && tasks.length === 0) {
|
|
210
|
-
return {
|
|
211
|
-
content: [
|
|
212
|
-
{
|
|
213
|
-
type: "text",
|
|
214
|
-
text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
details: {
|
|
218
|
-
projectAgentsDir,
|
|
219
|
-
results: [],
|
|
220
|
-
totalDurationMs: Date.now() - startTime,
|
|
267
|
+
// Check self-recursion prevention
|
|
268
|
+
if (blockedAgent && agentName === blockedAgent) {
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "text",
|
|
273
|
+
text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
221
274
|
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const available = agents.map((a) => a.name).join(", ");
|
|
230
|
-
return {
|
|
231
|
-
content: [{ type: "text", text: `Unknown agent: ${task.agent}. Available: ${available}` }],
|
|
232
|
-
details: {
|
|
233
|
-
projectAgentsDir,
|
|
234
|
-
results: [],
|
|
235
|
-
totalDurationMs: Date.now() - startTime,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
275
|
+
],
|
|
276
|
+
details: {
|
|
277
|
+
projectAgentsDir,
|
|
278
|
+
results: [],
|
|
279
|
+
totalDurationMs: Date.now() - startTime,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
239
282
|
}
|
|
240
283
|
|
|
241
284
|
// Check spawn restrictions from parent
|
|
242
285
|
const parentSpawns = session.getSessionSpawns() ?? "*";
|
|
243
286
|
const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
|
|
244
|
-
const isSpawnAllowed = (
|
|
287
|
+
const isSpawnAllowed = (): boolean => {
|
|
245
288
|
if (parentSpawns === "") return false; // Empty = deny all
|
|
246
289
|
if (parentSpawns === "*") return true; // Wildcard = allow all
|
|
247
290
|
return allowedSpawns.includes(agentName);
|
|
248
291
|
};
|
|
249
292
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
};
|
|
261
|
-
}
|
|
293
|
+
if (!isSpawnAllowed()) {
|
|
294
|
+
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
|
|
297
|
+
details: {
|
|
298
|
+
projectAgentsDir,
|
|
299
|
+
results: [],
|
|
300
|
+
totalDurationMs: Date.now() - startTime,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
262
303
|
}
|
|
263
304
|
|
|
264
|
-
// Build full prompts with context prepended
|
|
305
|
+
// Build full prompts with context prepended
|
|
265
306
|
const tasksWithContext = tasks.map((t) => ({
|
|
266
|
-
agent: t.agent,
|
|
267
307
|
task: context ? `${context}\n\n${t.task}` : t.task,
|
|
268
|
-
model: t.model,
|
|
269
308
|
description: t.description,
|
|
270
|
-
taskId:
|
|
309
|
+
taskId: t.id,
|
|
271
310
|
}));
|
|
272
311
|
|
|
273
312
|
// Initialize progress for all tasks
|
|
274
313
|
for (let i = 0; i < tasksWithContext.length; i++) {
|
|
275
314
|
const t = tasksWithContext[i];
|
|
276
|
-
const agentCfg = getAgent(agents, t.agent);
|
|
277
315
|
progressMap.set(i, {
|
|
278
316
|
index: i,
|
|
279
317
|
taskId: t.taskId,
|
|
280
|
-
agent:
|
|
281
|
-
agentSource:
|
|
318
|
+
agent: agentName,
|
|
319
|
+
agentSource: agent.source,
|
|
282
320
|
status: "pending",
|
|
283
321
|
task: t.task,
|
|
284
322
|
recentTools: [],
|
|
@@ -286,36 +324,40 @@ export async function createTaskTool(
|
|
|
286
324
|
toolCount: 0,
|
|
287
325
|
tokens: 0,
|
|
288
326
|
durationMs: 0,
|
|
289
|
-
modelOverride
|
|
327
|
+
modelOverride,
|
|
290
328
|
description: t.description,
|
|
291
329
|
});
|
|
292
330
|
}
|
|
293
331
|
emitProgress();
|
|
294
332
|
|
|
295
333
|
// Execute in parallel with concurrency limit
|
|
296
|
-
const results = await mapWithConcurrencyLimit(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
334
|
+
const results = await mapWithConcurrencyLimit(
|
|
335
|
+
tasksWithContext,
|
|
336
|
+
MAX_CONCURRENCY,
|
|
337
|
+
async (task, index) => {
|
|
338
|
+
return runSubprocess({
|
|
339
|
+
cwd: session.cwd,
|
|
340
|
+
agent,
|
|
341
|
+
task: task.task,
|
|
342
|
+
description: task.description,
|
|
343
|
+
index,
|
|
344
|
+
taskId: task.taskId,
|
|
345
|
+
context: undefined, // Already prepended above
|
|
346
|
+
modelOverride,
|
|
347
|
+
outputSchema,
|
|
348
|
+
sessionFile,
|
|
349
|
+
persistArtifacts: !!artifactsDir,
|
|
350
|
+
artifactsDir: effectiveArtifactsDir,
|
|
351
|
+
signal,
|
|
352
|
+
eventBus: undefined,
|
|
353
|
+
onProgress: (progress) => {
|
|
354
|
+
progressMap.set(index, structuredClone(progress));
|
|
355
|
+
emitProgress();
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
signal,
|
|
360
|
+
);
|
|
319
361
|
|
|
320
362
|
// Aggregate usage from executor results (already accumulated incrementally)
|
|
321
363
|
const aggregatedUsage = createUsageTotals();
|
|
@@ -349,14 +391,10 @@ export async function createTaskTool(
|
|
|
349
391
|
return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
|
|
350
392
|
});
|
|
351
393
|
|
|
352
|
-
const skippedNote =
|
|
353
|
-
skippedSelfRecursion > 0
|
|
354
|
-
? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
|
|
355
|
-
: "";
|
|
356
394
|
const outputIds = results.map((r) => r.taskId);
|
|
357
395
|
const outputHint =
|
|
358
396
|
outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
|
|
359
|
-
const summary = `${successCount}/${results.length} succeeded
|
|
397
|
+
const summary = `${successCount}/${results.length} succeeded [${formatDuration(
|
|
360
398
|
totalDuration,
|
|
361
399
|
)}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
|
|
362
400
|
|
|
@@ -7,24 +7,45 @@ import { MAX_CONCURRENCY } from "./types";
|
|
|
7
7
|
/**
|
|
8
8
|
* Execute items with a concurrency limit using a worker pool pattern.
|
|
9
9
|
* Results are returned in the same order as input items.
|
|
10
|
+
* Fails fast on first error - does not wait for other workers to complete.
|
|
10
11
|
*
|
|
11
12
|
* @param items - Items to process
|
|
12
13
|
* @param concurrency - Maximum concurrent operations
|
|
13
14
|
* @param fn - Async function to execute for each item
|
|
15
|
+
* @param signal - Optional abort signal to stop scheduling work
|
|
14
16
|
*/
|
|
15
17
|
export async function mapWithConcurrencyLimit<T, R>(
|
|
16
18
|
items: T[],
|
|
17
19
|
concurrency: number,
|
|
18
20
|
fn: (item: T, index: number) => Promise<R>,
|
|
21
|
+
signal?: AbortSignal,
|
|
19
22
|
): Promise<R[]> {
|
|
20
23
|
const limit = Math.max(1, Math.min(concurrency, items.length, MAX_CONCURRENCY));
|
|
21
24
|
const results: R[] = new Array(items.length);
|
|
22
25
|
let nextIndex = 0;
|
|
23
26
|
|
|
27
|
+
// Create internal abort controller to cancel workers on any rejection
|
|
28
|
+
const abortController = new AbortController();
|
|
29
|
+
const workerSignal = signal ? AbortSignal.any([signal, abortController.signal]) : abortController.signal;
|
|
30
|
+
|
|
31
|
+
// Promise that rejects on first error - used to fail fast
|
|
32
|
+
let rejectFirst: (error: unknown) => void;
|
|
33
|
+
const firstErrorPromise = new Promise<never>((_, reject) => {
|
|
34
|
+
rejectFirst = reject;
|
|
35
|
+
});
|
|
36
|
+
|
|
24
37
|
const worker = async (): Promise<void> => {
|
|
25
|
-
while (
|
|
38
|
+
while (true) {
|
|
39
|
+
workerSignal.throwIfAborted();
|
|
26
40
|
const index = nextIndex++;
|
|
27
|
-
|
|
41
|
+
if (index >= items.length) return;
|
|
42
|
+
try {
|
|
43
|
+
results[index] = await fn(items[index], index);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
abortController.abort();
|
|
46
|
+
rejectFirst(error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
28
49
|
}
|
|
29
50
|
};
|
|
30
51
|
|
|
@@ -32,7 +53,13 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
32
53
|
const workers = Array(limit)
|
|
33
54
|
.fill(null)
|
|
34
55
|
.map(() => worker());
|
|
56
|
+
await Promise.race([Promise.all(workers), firstErrorPromise]);
|
|
57
|
+
|
|
58
|
+
// Check external abort
|
|
59
|
+
if (signal?.aborted) {
|
|
60
|
+
const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
|
|
61
|
+
throw reason;
|
|
62
|
+
}
|
|
35
63
|
|
|
36
|
-
await Promise.all(workers);
|
|
37
64
|
return results;
|
|
38
65
|
}
|
|
@@ -58,12 +58,20 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
|
|
|
58
58
|
counts.set(finding.priority, (counts.get(finding.priority) ?? 0) + 1);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
const priorityMeta: Record<number, { icon: string; color: "error" | "warning" | "muted" | "accent" }> = {
|
|
62
|
+
0: { icon: theme.styledSymbol("status.error", "error"), color: "error" },
|
|
63
|
+
1: { icon: theme.styledSymbol("status.warning", "warning"), color: "warning" },
|
|
64
|
+
2: { icon: theme.styledSymbol("status.warning", "muted"), color: "muted" },
|
|
65
|
+
3: { icon: theme.styledSymbol("status.info", "accent"), color: "accent" },
|
|
66
|
+
};
|
|
67
|
+
|
|
61
68
|
const parts: string[] = [];
|
|
62
69
|
for (const priority of [0, 1, 2, 3]) {
|
|
63
70
|
const label = PRIORITY_LABELS[priority] ?? "P?";
|
|
64
|
-
const
|
|
71
|
+
const meta = priorityMeta[priority] ?? { icon: "", color: "muted" as const };
|
|
65
72
|
const count = counts.get(priority) ?? 0;
|
|
66
|
-
|
|
73
|
+
const text = theme.fg(meta.color, `${label}:${count}`);
|
|
74
|
+
parts.push(meta.icon ? `${meta.icon} ${text}` : text);
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
|
|
@@ -123,13 +131,19 @@ function renderJsonTreeLines(
|
|
|
123
131
|
pushLine(`${prefix}${iconArray} ${header}`);
|
|
124
132
|
if (val.length === 0) {
|
|
125
133
|
pushLine(
|
|
126
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
134
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
135
|
+
"dim",
|
|
136
|
+
"[]",
|
|
137
|
+
)}`,
|
|
127
138
|
);
|
|
128
139
|
return;
|
|
129
140
|
}
|
|
130
141
|
if (depth >= maxDepth) {
|
|
131
142
|
pushLine(
|
|
132
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
143
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
144
|
+
"dim",
|
|
145
|
+
theme.format.ellipsis,
|
|
146
|
+
)}`,
|
|
133
147
|
);
|
|
134
148
|
return;
|
|
135
149
|
}
|
|
@@ -150,13 +164,19 @@ function renderJsonTreeLines(
|
|
|
150
164
|
const entries = Object.entries(val as Record<string, unknown>);
|
|
151
165
|
if (entries.length === 0) {
|
|
152
166
|
pushLine(
|
|
153
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
167
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
168
|
+
"dim",
|
|
169
|
+
"{}",
|
|
170
|
+
)}`,
|
|
154
171
|
);
|
|
155
172
|
return;
|
|
156
173
|
}
|
|
157
174
|
if (depth >= maxDepth) {
|
|
158
175
|
pushLine(
|
|
159
|
-
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
176
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
|
|
177
|
+
"dim",
|
|
178
|
+
theme.format.ellipsis,
|
|
179
|
+
)}`,
|
|
160
180
|
);
|
|
161
181
|
return;
|
|
162
182
|
}
|
|
@@ -233,19 +253,25 @@ function renderOutputSection(
|
|
|
233
253
|
*/
|
|
234
254
|
export function renderCall(args: TaskParams, theme: Theme): Component {
|
|
235
255
|
const label = theme.fg("toolTitle", theme.bold("Task"));
|
|
256
|
+
const agentTag = theme.italic(
|
|
257
|
+
theme.fg("dim", `${theme.format.bracketLeft}${args.agent}${theme.format.bracketRight}`),
|
|
258
|
+
);
|
|
236
259
|
|
|
237
260
|
if (args.tasks.length === 1) {
|
|
238
|
-
// Single task - show
|
|
261
|
+
// Single task - show description preview
|
|
239
262
|
const task = args.tasks[0];
|
|
240
|
-
const summary = task.description
|
|
241
|
-
const taskPreview = truncate(summary,
|
|
242
|
-
return new Text(`${label} ${
|
|
263
|
+
const summary = task.description.trim() || task.task;
|
|
264
|
+
const taskPreview = truncate(summary, 50, theme.format.ellipsis);
|
|
265
|
+
return new Text(`${label} ${agentTag} ${theme.fg("muted", taskPreview)}`, 0, 0);
|
|
243
266
|
}
|
|
244
267
|
|
|
245
|
-
// Multiple tasks - show count and descriptions
|
|
246
|
-
const
|
|
268
|
+
// Multiple tasks - show count and descriptions
|
|
269
|
+
const descriptions = args.tasks.map((t) => t.description.trim()).join(", ");
|
|
247
270
|
return new Text(
|
|
248
|
-
`${label} ${
|
|
271
|
+
`${label} ${agentTag} ${args.tasks.length} agents: ${theme.fg(
|
|
272
|
+
"muted",
|
|
273
|
+
truncate(descriptions, 50, theme.format.ellipsis),
|
|
274
|
+
)}`,
|
|
249
275
|
0,
|
|
250
276
|
0,
|
|
251
277
|
);
|
|
@@ -275,23 +301,14 @@ function renderAgentProgress(
|
|
|
275
301
|
? "error"
|
|
276
302
|
: "accent";
|
|
277
303
|
|
|
278
|
-
// Main status line
|
|
279
|
-
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.taskId)}`;
|
|
304
|
+
// Main status line: taskId: description [status] · stats · ⟨agent⟩
|
|
280
305
|
const description = progress.description?.trim();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
306
|
+
const titlePart = description ? `${theme.bold(progress.taskId)}: ${description}` : progress.taskId;
|
|
307
|
+
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
284
308
|
|
|
285
309
|
// Only show badge for non-running states (spinner already indicates running)
|
|
286
|
-
if (progress.status
|
|
287
|
-
const statusLabel =
|
|
288
|
-
progress.status === "completed"
|
|
289
|
-
? "done"
|
|
290
|
-
: progress.status === "failed"
|
|
291
|
-
? "failed"
|
|
292
|
-
: progress.status === "aborted"
|
|
293
|
-
? "aborted"
|
|
294
|
-
: "pending";
|
|
310
|
+
if (progress.status === "failed" || progress.status === "aborted") {
|
|
311
|
+
const statusLabel = progress.status === "failed" ? "failed" : "aborted";
|
|
295
312
|
statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
|
|
296
313
|
}
|
|
297
314
|
|
|
@@ -338,6 +355,21 @@ function renderAgentProgress(
|
|
|
338
355
|
|
|
339
356
|
// Render extracted tool data inline (e.g., review findings)
|
|
340
357
|
if (progress.extractedToolData) {
|
|
358
|
+
// For completed tasks, check for review verdict from complete tool
|
|
359
|
+
if (progress.status === "completed") {
|
|
360
|
+
const completeData = progress.extractedToolData.complete as Array<{ data: unknown }> | undefined;
|
|
361
|
+
const reportFindingData = progress.extractedToolData.report_finding as ReportFindingDetails[] | undefined;
|
|
362
|
+
const reviewData = completeData
|
|
363
|
+
?.map((c) => c.data as SubmitReviewDetails)
|
|
364
|
+
.filter((d) => d && typeof d === "object" && "overall_correctness" in d);
|
|
365
|
+
if (reviewData && reviewData.length > 0) {
|
|
366
|
+
const summary = reviewData[reviewData.length - 1];
|
|
367
|
+
const findings = reportFindingData ?? [];
|
|
368
|
+
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
369
|
+
return lines; // Review result handles its own rendering
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
341
373
|
for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
|
|
342
374
|
const handler = subprocessToolRegistry.getHandler(toolName);
|
|
343
375
|
if (handler?.renderInline) {
|
|
@@ -381,7 +413,10 @@ function renderReviewResult(
|
|
|
381
413
|
const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
|
|
382
414
|
const verdictIcon = summary.overall_correctness === "correct" ? theme.status.success : theme.status.error;
|
|
383
415
|
lines.push(
|
|
384
|
-
`${continuePrefix}
|
|
416
|
+
`${continuePrefix} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg(
|
|
417
|
+
verdictColor,
|
|
418
|
+
verdictIcon,
|
|
419
|
+
)} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
|
|
385
420
|
);
|
|
386
421
|
|
|
387
422
|
// Explanation preview (first ~80 chars when collapsed, full when expanded)
|
|
@@ -395,7 +430,7 @@ function renderReviewResult(
|
|
|
395
430
|
} else {
|
|
396
431
|
// Preview: first sentence or ~100 chars
|
|
397
432
|
const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100, theme.format.ellipsis);
|
|
398
|
-
lines.push(`${continuePrefix}${theme.fg("dim",
|
|
433
|
+
lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
|
|
399
434
|
}
|
|
400
435
|
}
|
|
401
436
|
|
|
@@ -411,7 +446,7 @@ function renderReviewResult(
|
|
|
411
446
|
}
|
|
412
447
|
|
|
413
448
|
/**
|
|
414
|
-
* Render review findings list
|
|
449
|
+
* Render review findings list.
|
|
415
450
|
*/
|
|
416
451
|
function renderFindings(
|
|
417
452
|
findings: ReportFindingDetails[],
|
|
@@ -472,12 +507,14 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
472
507
|
const iconColor = success ? "success" : "error";
|
|
473
508
|
const statusText = aborted ? "aborted" : success ? "done" : "failed";
|
|
474
509
|
|
|
475
|
-
// Main status line
|
|
476
|
-
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.taskId)} ${formatBadge(statusText, iconColor, theme)}`;
|
|
510
|
+
// Main status line: taskId: description [status] · stats · ⟨agent⟩
|
|
477
511
|
const description = result.description?.trim();
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
512
|
+
const titlePart = description ? `${theme.bold(result.taskId)}: ${description}` : result.taskId;
|
|
513
|
+
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge(
|
|
514
|
+
statusText,
|
|
515
|
+
iconColor,
|
|
516
|
+
theme,
|
|
517
|
+
)}`;
|
|
481
518
|
if (result.tokens > 0) {
|
|
482
519
|
statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
|
|
483
520
|
}
|
|
@@ -489,10 +526,16 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
489
526
|
|
|
490
527
|
lines.push(statusLine);
|
|
491
528
|
|
|
492
|
-
// Check for review result (
|
|
493
|
-
const
|
|
529
|
+
// Check for review result (complete with review schema + report_finding)
|
|
530
|
+
const completeData = result.extractedToolData?.complete as Array<{ data: unknown }> | undefined;
|
|
494
531
|
const reportFindingData = result.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
|
|
495
532
|
|
|
533
|
+
// Extract review verdict from complete tool's data field if it matches SubmitReviewDetails
|
|
534
|
+
const reviewData = completeData
|
|
535
|
+
?.map((c) => c.data as SubmitReviewDetails)
|
|
536
|
+
.filter((d) => d && typeof d === "object" && "overall_correctness" in d);
|
|
537
|
+
const submitReviewData = reviewData && reviewData.length > 0 ? reviewData : undefined;
|
|
538
|
+
|
|
496
539
|
if (submitReviewData && submitReviewData.length > 0) {
|
|
497
540
|
// Use combined review renderer
|
|
498
541
|
const summary = submitReviewData[submitReviewData.length - 1];
|
|
@@ -502,7 +545,10 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
502
545
|
}
|
|
503
546
|
if (reportFindingData && reportFindingData.length > 0) {
|
|
504
547
|
lines.push(
|
|
505
|
-
`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
548
|
+
`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
549
|
+
"dim",
|
|
550
|
+
"Review summary missing (complete not called)",
|
|
551
|
+
)}`,
|
|
506
552
|
);
|
|
507
553
|
lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
|
|
508
554
|
lines.push(`${continuePrefix}`); // Spacing
|
|
@@ -515,7 +561,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
515
561
|
if (result.extractedToolData) {
|
|
516
562
|
for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
|
|
517
563
|
// Skip review tools - handled above
|
|
518
|
-
if (toolName === "
|
|
564
|
+
if (toolName === "complete" || toolName === "report_finding") continue;
|
|
519
565
|
|
|
520
566
|
const handler = subprocessToolRegistry.getHandler(toolName);
|
|
521
567
|
if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
|
|
@@ -41,20 +41,29 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
|
|
|
41
41
|
|
|
42
42
|
/** Single task item for parallel execution */
|
|
43
43
|
export const taskItemSchema = Type.Object({
|
|
44
|
-
|
|
44
|
+
id: Type.String({
|
|
45
|
+
description: "Short task identifier for display (max 32 chars, CamelCase, e.g. 'SessionStore', 'WebFetchFix')",
|
|
46
|
+
maxLength: 32,
|
|
47
|
+
pattern: "^[A-Za-z][A-Za-z0-9]*$",
|
|
48
|
+
}),
|
|
45
49
|
task: Type.String({ description: "Task description for the agent" }),
|
|
46
|
-
description: Type.
|
|
47
|
-
model: Type.Optional(Type.String({ description: "Model override for this task" })),
|
|
50
|
+
description: Type.String({ description: "Short description for UI display" }),
|
|
48
51
|
});
|
|
49
52
|
|
|
50
53
|
export type TaskItem = Static<typeof taskItemSchema>;
|
|
51
54
|
|
|
52
55
|
/** Task tool parameters */
|
|
53
56
|
export const taskSchema = Type.Object({
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
agent: Type.String({ description: "Agent type to use for all tasks" }),
|
|
58
|
+
context: Type.String({ description: "Shared context prepended to all task prompts" }),
|
|
59
|
+
model: Type.Optional(
|
|
60
|
+
Type.String({
|
|
61
|
+
description: "Model override for all tasks (fuzzy matching, e.g. 'sonnet', 'opus')",
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
output: Type.Optional(
|
|
56
65
|
Type.Any({
|
|
57
|
-
description: "
|
|
66
|
+
description: "JTD schema for structured subagent output (used by the complete tool)",
|
|
58
67
|
}),
|
|
59
68
|
),
|
|
60
69
|
tasks: Type.Array(taskItemSchema, {
|