@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 +90 -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 +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- 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/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/complete.ts +2 -4
- 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 +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- 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 +55 -32
- 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 +152 -76
- 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/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- 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 +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- 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-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -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-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- 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-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- 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-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -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-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- 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-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- 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 -38
- 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/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
|
@@ -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) {
|
|
@@ -4,20 +4,34 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
4
4
|
/** Source of an agent definition */
|
|
5
5
|
export type AgentSource = "bundled" | "user" | "project";
|
|
6
6
|
|
|
7
|
+
function getEnv(name: string, defaultValue: number): number {
|
|
8
|
+
const value = process.env[name];
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return defaultValue;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const number = Number.parseInt(value, 10);
|
|
14
|
+
if (!Number.isNaN(number) && number > 0) {
|
|
15
|
+
return number;
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
return defaultValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/** Maximum tasks per call */
|
|
8
|
-
export const MAX_PARALLEL_TASKS = 32;
|
|
22
|
+
export const MAX_PARALLEL_TASKS = getEnv("OMP_TASK_MAX_PARALLEL", 32);
|
|
9
23
|
|
|
10
24
|
/** Maximum concurrent workers */
|
|
11
|
-
export const MAX_CONCURRENCY = 16;
|
|
25
|
+
export const MAX_CONCURRENCY = getEnv("OMP_TASK_MAX_CONCURRENCY", 16);
|
|
12
26
|
|
|
13
27
|
/** Maximum output bytes per agent */
|
|
14
|
-
export const MAX_OUTPUT_BYTES = 500_000;
|
|
28
|
+
export const MAX_OUTPUT_BYTES = getEnv("OMP_TASK_MAX_OUTPUT_BYTES", 500_000);
|
|
15
29
|
|
|
16
30
|
/** Maximum output lines per agent */
|
|
17
|
-
export const MAX_OUTPUT_LINES = 5000;
|
|
31
|
+
export const MAX_OUTPUT_LINES = getEnv("OMP_TASK_MAX_OUTPUT_LINES", 5000);
|
|
18
32
|
|
|
19
33
|
/** Maximum agents to show in description */
|
|
20
|
-
export const MAX_AGENTS_IN_DESCRIPTION = 10;
|
|
34
|
+
export const MAX_AGENTS_IN_DESCRIPTION = getEnv("OMP_TASK_MAX_AGENTS_IN_DESCRIPTION", 10);
|
|
21
35
|
|
|
22
36
|
/** EventBus channel for raw subagent events */
|
|
23
37
|
export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
|
|
@@ -27,20 +41,29 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
|
|
|
27
41
|
|
|
28
42
|
/** Single task item for parallel execution */
|
|
29
43
|
export const taskItemSchema = Type.Object({
|
|
30
|
-
|
|
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
|
+
}),
|
|
31
49
|
task: Type.String({ description: "Task description for the agent" }),
|
|
32
|
-
description: Type.
|
|
33
|
-
model: Type.Optional(Type.String({ description: "Model override for this task" })),
|
|
50
|
+
description: Type.String({ description: "Short description for UI display" }),
|
|
34
51
|
});
|
|
35
52
|
|
|
36
53
|
export type TaskItem = Static<typeof taskItemSchema>;
|
|
37
54
|
|
|
38
55
|
/** Task tool parameters */
|
|
39
56
|
export const taskSchema = Type.Object({
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
42
65
|
Type.Any({
|
|
43
|
-
description: "
|
|
66
|
+
description: "JTD schema for structured subagent output (used by the complete tool)",
|
|
44
67
|
}),
|
|
45
68
|
),
|
|
46
69
|
tasks: Type.Array(taskItemSchema, {
|
|
@@ -19,12 +19,17 @@ import type { AgentSessionEvent } from "../../agent-session";
|
|
|
19
19
|
import { parseModelPattern, parseModelString } from "../../model-resolver";
|
|
20
20
|
import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
|
|
21
21
|
import { SessionManager } from "../../session-manager";
|
|
22
|
+
import { untilAborted } from "../../utils";
|
|
22
23
|
import type { SubagentWorkerRequest, SubagentWorkerResponse, SubagentWorkerStartPayload } from "./worker-protocol";
|
|
23
24
|
|
|
24
25
|
type PostMessageFn = (message: SubagentWorkerResponse) => void;
|
|
25
26
|
|
|
26
27
|
const postMessageSafe: PostMessageFn = (message) => {
|
|
27
|
-
|
|
28
|
+
try {
|
|
29
|
+
(globalThis as typeof globalThis & { postMessage: PostMessageFn }).postMessage(message);
|
|
30
|
+
} catch {
|
|
31
|
+
// Parent may have terminated worker, nothing we can do
|
|
32
|
+
}
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
interface WorkerMessageEvent<T> {
|
|
@@ -49,9 +54,33 @@ const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent => {
|
|
|
49
54
|
return agentEventTypes.has(event.type as AgentEvent["type"]);
|
|
50
55
|
};
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
interface RunState {
|
|
58
|
+
abortController: AbortController;
|
|
59
|
+
startTime: number;
|
|
60
|
+
session: { abort: () => Promise<void>; dispose: () => Promise<void> } | null;
|
|
61
|
+
unsubscribe: (() => void) | null;
|
|
62
|
+
sendDoneOnce: (message: Extract<SubagentWorkerResponse, { type: "done" }>) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const createSendDoneOnce = (): RunState["sendDoneOnce"] => {
|
|
66
|
+
let sent = false;
|
|
67
|
+
return (message) => {
|
|
68
|
+
if (sent) return;
|
|
69
|
+
sent = true;
|
|
70
|
+
postMessageSafe(message);
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const createRunState = (): RunState => ({
|
|
75
|
+
abortController: new AbortController(),
|
|
76
|
+
startTime: Date.now(),
|
|
77
|
+
session: null,
|
|
78
|
+
unsubscribe: null,
|
|
79
|
+
sendDoneOnce: createSendDoneOnce(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let activeRun: RunState | null = null;
|
|
83
|
+
let pendingAbort = false;
|
|
55
84
|
|
|
56
85
|
/**
|
|
57
86
|
* Resolve model string to Model object with optional thinking level.
|
|
@@ -92,26 +121,35 @@ function resolveModelOverride(
|
|
|
92
121
|
* - OMP_BLOCKED_AGENT: payload.blockedAgent (prevents same-agent recursion)
|
|
93
122
|
* - OMP_SPAWNS: payload.spawnsEnv (controls nested spawn permissions)
|
|
94
123
|
*/
|
|
95
|
-
async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
96
|
-
const
|
|
124
|
+
async function runTask(runState: RunState, payload: SubagentWorkerStartPayload): Promise<void> {
|
|
125
|
+
const { signal } = runState.abortController;
|
|
126
|
+
const startTime = runState.startTime;
|
|
97
127
|
let exitCode = 0;
|
|
98
128
|
let error: string | undefined;
|
|
99
129
|
let aborted = false;
|
|
130
|
+
const sessionAbortController = new AbortController();
|
|
100
131
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
132
|
+
// Helper to check abort status - throws if aborted to exit early
|
|
133
|
+
const checkAbort = (): void => {
|
|
134
|
+
if (signal.aborted) {
|
|
104
135
|
aborted = true;
|
|
105
136
|
exitCode = 1;
|
|
106
|
-
|
|
137
|
+
throw new Error("Aborted");
|
|
107
138
|
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Check for pre-start abort
|
|
143
|
+
checkAbort();
|
|
108
144
|
|
|
109
145
|
// Set working directory (CLI does this implicitly)
|
|
110
146
|
process.chdir(payload.cwd);
|
|
111
147
|
|
|
112
148
|
// Discover auth and models (equivalent to CLI's discoverAuthStorage/discoverModels)
|
|
113
149
|
const authStorage = await discoverAuthStorage();
|
|
150
|
+
checkAbort();
|
|
114
151
|
const modelRegistry = await discoverModels(authStorage);
|
|
152
|
+
checkAbort();
|
|
115
153
|
|
|
116
154
|
// Resolve model override (equivalent to CLI's parseModelPattern with --model)
|
|
117
155
|
const { model, thinkingLevel } = resolveModelOverride(payload.model, modelRegistry);
|
|
@@ -120,6 +158,7 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
120
158
|
const sessionManager = payload.sessionFile
|
|
121
159
|
? await SessionManager.open(payload.sessionFile)
|
|
122
160
|
: SessionManager.inMemory(payload.cwd);
|
|
161
|
+
checkAbort();
|
|
123
162
|
|
|
124
163
|
// Create agent session (equivalent to CLI's createAgentSession)
|
|
125
164
|
// Note: hasUI: false disables interactive features
|
|
@@ -143,7 +182,16 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
143
182
|
spawns: payload.spawnsEnv,
|
|
144
183
|
});
|
|
145
184
|
|
|
146
|
-
|
|
185
|
+
runState.session = session;
|
|
186
|
+
checkAbort();
|
|
187
|
+
|
|
188
|
+
signal.addEventListener(
|
|
189
|
+
"abort",
|
|
190
|
+
() => {
|
|
191
|
+
void session.abort();
|
|
192
|
+
},
|
|
193
|
+
{ once: true, signal: sessionAbortController.signal },
|
|
194
|
+
);
|
|
147
195
|
|
|
148
196
|
// Initialize extensions (equivalent to CLI's extension initialization)
|
|
149
197
|
// Note: Does not support --extension CLI flag or extension CLI flags
|
|
@@ -174,7 +222,7 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
174
222
|
let completeCalled = false;
|
|
175
223
|
|
|
176
224
|
// Subscribe to events and forward to parent (equivalent to --mode json output)
|
|
177
|
-
session.subscribe((event: AgentSessionEvent) => {
|
|
225
|
+
runState.unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
178
226
|
if (isAgentEvent(event)) {
|
|
179
227
|
postMessageSafe({ type: "event", event });
|
|
180
228
|
// Track when complete tool is called
|
|
@@ -189,7 +237,7 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
|
|
|
189
237
|
|
|
190
238
|
// Retry loop if complete was not called
|
|
191
239
|
let retryCount = 0;
|
|
192
|
-
while (!completeCalled && retryCount < MAX_COMPLETE_RETRIES && !
|
|
240
|
+
while (!completeCalled && retryCount < MAX_COMPLETE_RETRIES && !signal.aborted) {
|
|
193
241
|
retryCount++;
|
|
194
242
|
const reminder = `<system-reminder>
|
|
195
243
|
CRITICAL: You stopped without calling the complete tool. This is reminder ${retryCount} of ${MAX_COMPLETE_RETRIES}.
|
|
@@ -214,26 +262,45 @@ Call complete now.`;
|
|
|
214
262
|
}
|
|
215
263
|
} catch (err) {
|
|
216
264
|
exitCode = 1;
|
|
217
|
-
|
|
265
|
+
// Don't record abort as error - it's handled via the aborted flag
|
|
266
|
+
if (!signal.aborted) {
|
|
267
|
+
error = err instanceof Error ? err.stack || err.message : String(err);
|
|
268
|
+
}
|
|
218
269
|
} finally {
|
|
219
270
|
// Handle abort requested during execution
|
|
220
|
-
if (
|
|
271
|
+
if (signal.aborted) {
|
|
221
272
|
aborted = true;
|
|
222
273
|
if (exitCode === 0) exitCode = 1;
|
|
223
274
|
}
|
|
224
275
|
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
sessionAbortController.abort();
|
|
277
|
+
|
|
278
|
+
if (runState.unsubscribe) {
|
|
279
|
+
try {
|
|
280
|
+
runState.unsubscribe();
|
|
281
|
+
} catch {
|
|
282
|
+
// Ignore unsubscribe errors
|
|
283
|
+
}
|
|
284
|
+
runState.unsubscribe = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Cleanup session with timeout to prevent hanging
|
|
288
|
+
if (runState.session) {
|
|
289
|
+
const session = runState.session;
|
|
290
|
+
runState.session = null;
|
|
227
291
|
try {
|
|
228
|
-
await
|
|
292
|
+
await untilAborted(AbortSignal.timeout(5000), () => session.dispose());
|
|
229
293
|
} catch {
|
|
230
294
|
// Ignore cleanup errors
|
|
231
295
|
}
|
|
232
|
-
activeSession = null;
|
|
233
296
|
}
|
|
234
297
|
|
|
235
|
-
|
|
236
|
-
|
|
298
|
+
if (activeRun === runState) {
|
|
299
|
+
activeRun = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Send completion message to parent (only once)
|
|
303
|
+
runState.sendDoneOnce({
|
|
237
304
|
type: "done",
|
|
238
305
|
exitCode,
|
|
239
306
|
durationMs: Date.now() - startTime,
|
|
@@ -245,12 +312,65 @@ Call complete now.`;
|
|
|
245
312
|
|
|
246
313
|
/** Handle abort request from parent */
|
|
247
314
|
function handleAbort(): void {
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
315
|
+
const runState = activeRun;
|
|
316
|
+
if (!runState) {
|
|
317
|
+
pendingAbort = true;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
runState.abortController.abort();
|
|
321
|
+
if (runState.session) {
|
|
322
|
+
void runState.session.abort();
|
|
251
323
|
}
|
|
252
324
|
}
|
|
253
325
|
|
|
326
|
+
const reportFatal = (message: string): void => {
|
|
327
|
+
const runState = activeRun;
|
|
328
|
+
if (runState) {
|
|
329
|
+
runState.abortController.abort();
|
|
330
|
+
if (runState.session) {
|
|
331
|
+
void runState.session.abort();
|
|
332
|
+
}
|
|
333
|
+
runState.sendDoneOnce({
|
|
334
|
+
type: "done",
|
|
335
|
+
exitCode: 1,
|
|
336
|
+
durationMs: Date.now() - runState.startTime,
|
|
337
|
+
error: message,
|
|
338
|
+
aborted: false,
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
postMessageSafe({
|
|
344
|
+
type: "done",
|
|
345
|
+
exitCode: 1,
|
|
346
|
+
durationMs: 0,
|
|
347
|
+
error: message,
|
|
348
|
+
aborted: false,
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Global error handlers to ensure we always send a done message
|
|
353
|
+
// Using self instead of globalThis for proper worker scope typing
|
|
354
|
+
declare const self: {
|
|
355
|
+
addEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
|
|
356
|
+
addEventListener(type: "unhandledrejection", listener: (event: { reason: unknown }) => void): void;
|
|
357
|
+
addEventListener(type: "messageerror", listener: (event: MessageEvent) => void): void;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
self.addEventListener("error", (event) => {
|
|
361
|
+
reportFatal(`Uncaught error: ${event.message || "Unknown error"}`);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
self.addEventListener("unhandledrejection", (event) => {
|
|
365
|
+
const reason = event.reason;
|
|
366
|
+
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
367
|
+
reportFatal(`Unhandled rejection: ${message}`);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
self.addEventListener("messageerror", () => {
|
|
371
|
+
reportFatal("Failed to deserialize parent message");
|
|
372
|
+
});
|
|
373
|
+
|
|
254
374
|
// Message handler - receives start/abort commands from parent
|
|
255
375
|
globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorkerRequest>) => {
|
|
256
376
|
const message = event.data;
|
|
@@ -263,8 +383,13 @@ globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorker
|
|
|
263
383
|
|
|
264
384
|
if (message.type === "start") {
|
|
265
385
|
// Only allow one task per worker
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
386
|
+
if (activeRun) return;
|
|
387
|
+
const runState = createRunState();
|
|
388
|
+
if (pendingAbort) {
|
|
389
|
+
pendingAbort = false;
|
|
390
|
+
runState.abortController.abort();
|
|
391
|
+
}
|
|
392
|
+
activeRun = runState;
|
|
393
|
+
void runTask(runState, message.payload);
|
|
269
394
|
}
|
|
270
395
|
});
|