tau-coding-agent 0.1.5 → 0.2.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/README.md +15 -12
- package/extensions/answer.ts +129 -73
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +90 -4
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +138 -183
- package/extensions/loop.ts +150 -135
- package/extensions/memory.ts +172 -97
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +43 -40
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +138 -2850
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +250 -357
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +44 -32
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +4 -4
- package/skills/oracle/scripts/oracle +64 -51
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
|
@@ -1,24 +1,22 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1
2
|
import {
|
|
2
3
|
keyText,
|
|
3
4
|
type ExtensionCommandContext,
|
|
4
5
|
type ExtensionContext,
|
|
5
6
|
type ExtensionAPI,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
getKeybindings,
|
|
9
|
-
isKeyRelease,
|
|
10
|
-
truncateToWidth,
|
|
11
|
-
visibleWidth,
|
|
12
|
-
type Component,
|
|
13
|
-
} from "@earendil-works/pi-tui";
|
|
8
|
+
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
14
9
|
|
|
15
10
|
import {
|
|
16
11
|
buildAdditionalContextSection,
|
|
17
12
|
buildProjectReviewGuidelinesSection,
|
|
18
13
|
REVIEW_DEDUP_PROMPT,
|
|
14
|
+
REVIEW_DIFF_SCOPE_PROMPT,
|
|
19
15
|
REVIEW_FOCUSES,
|
|
20
16
|
REVIEW_FOCUS_PROMPT,
|
|
21
17
|
REVIEW_OUTPUT_CONTRACT_PROMPT,
|
|
18
|
+
REVIEW_RUBRIC_PROMPT,
|
|
19
|
+
REVIEW_SNAPSHOT_SCOPE_PROMPT,
|
|
22
20
|
} from "./prompts.js";
|
|
23
21
|
import {
|
|
24
22
|
buildReviewedScopeLine,
|
|
@@ -72,13 +70,15 @@ import {
|
|
|
72
70
|
} from "./models.js";
|
|
73
71
|
import {
|
|
74
72
|
notify,
|
|
73
|
+
publishReviewActivity,
|
|
75
74
|
REVIEW_CANCELLED_ERROR,
|
|
76
|
-
|
|
75
|
+
REVIEW_PROGRESS_WIDGET_KEY,
|
|
76
|
+
renderReviewProgressHeader,
|
|
77
77
|
STATUS_SPINNER_FRAMES,
|
|
78
78
|
STATUS_SPINNER_INTERVAL_MS,
|
|
79
|
-
|
|
79
|
+
joinAll,
|
|
80
80
|
withSpinner,
|
|
81
|
-
type
|
|
81
|
+
type ReviewRuntime,
|
|
82
82
|
} from "./runtime.js";
|
|
83
83
|
import {
|
|
84
84
|
REVIEW_FOCUS_NAMES,
|
|
@@ -111,8 +111,6 @@ const REVIEW_STALE_FIX_NEXT_STEP =
|
|
|
111
111
|
const REVIEW_STALE_PAYLOAD_WARNING = "Repository changed since this review was generated.";
|
|
112
112
|
const REVIEW_STALE_PAYLOAD_NEXT_STEP = "Run /review first to refresh it.";
|
|
113
113
|
|
|
114
|
-
const REVIEW_PROGRESS_WIDGET_KEY = "review-progress";
|
|
115
|
-
|
|
116
114
|
type ReviewTheme = ExtensionContext["ui"]["theme"];
|
|
117
115
|
|
|
118
116
|
type FocusTask = {
|
|
@@ -148,7 +146,6 @@ type ReviewProgressTask = {
|
|
|
148
146
|
|
|
149
147
|
type ReviewProgressState = {
|
|
150
148
|
startedAtMs: number;
|
|
151
|
-
expanded: boolean;
|
|
152
149
|
frame: number;
|
|
153
150
|
tasks: ReviewProgressTask[];
|
|
154
151
|
};
|
|
@@ -166,7 +163,11 @@ type PreparedReviewRun = {
|
|
|
166
163
|
tasks: FocusTask[];
|
|
167
164
|
};
|
|
168
165
|
|
|
169
|
-
function createReviewProgress(
|
|
166
|
+
function createReviewProgress(
|
|
167
|
+
pi: ExtensionAPI,
|
|
168
|
+
ctx: ExtensionContext,
|
|
169
|
+
tasks: FocusTask[],
|
|
170
|
+
): ReviewProgressController {
|
|
170
171
|
if (!ctx.hasUI) {
|
|
171
172
|
return {
|
|
172
173
|
update: () => {},
|
|
@@ -176,7 +177,6 @@ function createReviewProgress(ctx: ExtensionContext, tasks: FocusTask[]): Review
|
|
|
176
177
|
|
|
177
178
|
const state: ReviewProgressState = {
|
|
178
179
|
startedAtMs: Date.now(),
|
|
179
|
-
expanded: false,
|
|
180
180
|
frame: 0,
|
|
181
181
|
tasks: tasks.map((task) => ({
|
|
182
182
|
focus: task.focus,
|
|
@@ -186,27 +186,35 @@ function createReviewProgress(ctx: ExtensionContext, tasks: FocusTask[]): Review
|
|
|
186
186
|
};
|
|
187
187
|
const progressTasks = new Map(tasks.map((task, index) => [task, state.tasks[index]!]));
|
|
188
188
|
|
|
189
|
+
let requestRender: (() => void) | undefined;
|
|
190
|
+
if (ctx.mode === "tui") {
|
|
191
|
+
ctx.ui.setWidget(
|
|
192
|
+
REVIEW_PROGRESS_WIDGET_KEY,
|
|
193
|
+
(tui, theme) => {
|
|
194
|
+
requestRender = () => tui.requestRender();
|
|
195
|
+
return new ReviewProgressComponent(
|
|
196
|
+
state,
|
|
197
|
+
theme,
|
|
198
|
+
() => ctx.ui.getToolsExpanded(),
|
|
199
|
+
() => publishReviewActivity(pi, ctx, `review ${buildReviewProgressStatus(state)}`),
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
{ placement: "aboveEditor" },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
189
205
|
const render = () => {
|
|
190
|
-
if (
|
|
191
|
-
|
|
206
|
+
if (ctx.mode === "tui") {
|
|
207
|
+
publishReviewActivity(pi, ctx, `review ${buildReviewProgressStatus(state)}`);
|
|
208
|
+
requestRender?.();
|
|
209
|
+
} else
|
|
192
210
|
ctx.ui.setWidget(
|
|
193
211
|
REVIEW_PROGRESS_WIDGET_KEY,
|
|
194
|
-
|
|
195
|
-
{
|
|
212
|
+
[`Review · ${buildReviewProgressStatus(state)}`],
|
|
213
|
+
{
|
|
214
|
+
placement: "aboveEditor",
|
|
215
|
+
},
|
|
196
216
|
);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
201
|
-
ctx.ui.setStatus(REVIEW_STATUS_KEY, buildCollapsedReviewProgressStatus(state));
|
|
202
217
|
};
|
|
203
|
-
const unsubscribeToggle = ctx.ui.onTerminalInput((data) => {
|
|
204
|
-
if (isKeyRelease(data)) return undefined;
|
|
205
|
-
if (!getKeybindings().matches(data, "app.tools.expand")) return undefined;
|
|
206
|
-
state.expanded = !state.expanded;
|
|
207
|
-
render();
|
|
208
|
-
return { consume: true };
|
|
209
|
-
});
|
|
210
218
|
const timer = setInterval(() => {
|
|
211
219
|
state.frame = (state.frame + 1) % STATUS_SPINNER_FRAMES.length;
|
|
212
220
|
render();
|
|
@@ -222,9 +230,8 @@ function createReviewProgress(ctx: ExtensionContext, tasks: FocusTask[]): Review
|
|
|
222
230
|
},
|
|
223
231
|
stop: () => {
|
|
224
232
|
clearInterval(timer);
|
|
225
|
-
|
|
226
|
-
ctx.ui.
|
|
227
|
-
ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
233
|
+
publishReviewActivity(pi, ctx, "review");
|
|
234
|
+
ctx.ui.setWidget(REVIEW_PROGRESS_WIDGET_KEY, undefined);
|
|
228
235
|
},
|
|
229
236
|
};
|
|
230
237
|
}
|
|
@@ -233,85 +240,82 @@ class ReviewProgressComponent implements Component {
|
|
|
233
240
|
constructor(
|
|
234
241
|
private state: ReviewProgressState,
|
|
235
242
|
private theme: ReviewTheme,
|
|
243
|
+
private isExpanded: () => boolean,
|
|
244
|
+
private publishActivity: () => boolean,
|
|
236
245
|
) {}
|
|
237
246
|
|
|
238
247
|
invalidate(): void {}
|
|
239
248
|
|
|
240
249
|
render(width: number): string[] {
|
|
241
|
-
|
|
250
|
+
const handled = this.publishActivity();
|
|
251
|
+
if (!this.isExpanded() && handled) return [];
|
|
252
|
+
const hint = [formatDuration(Date.now() - this.state.startedAtMs), keyText("app.tools.expand")]
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join(" · ");
|
|
255
|
+
const header = renderReviewProgressHeader(
|
|
256
|
+
buildReviewProgressStatus(this.state),
|
|
257
|
+
this.theme,
|
|
258
|
+
width,
|
|
259
|
+
hint,
|
|
260
|
+
);
|
|
261
|
+
if (!this.isExpanded()) return [header];
|
|
262
|
+
return [
|
|
263
|
+
header,
|
|
264
|
+
...buildReviewProgressRows(
|
|
265
|
+
groupReviewProgressTasks(this.state.tasks),
|
|
266
|
+
this.theme,
|
|
267
|
+
width,
|
|
268
|
+
getProgressSpinner(this.state),
|
|
269
|
+
),
|
|
270
|
+
];
|
|
242
271
|
}
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
function
|
|
274
|
+
function buildReviewProgressStatus(state: ReviewProgressState): string {
|
|
246
275
|
const counts = countReviewProgressTasks(state.tasks);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
return `${spinner} reviewing ${counts.finished}/${counts.total}${failedText} (${keyText("app.tools.expand")} to expand)`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function buildExpandedReviewProgressLines(
|
|
253
|
-
state: ReviewProgressState,
|
|
254
|
-
theme: ReviewTheme,
|
|
255
|
-
width: number,
|
|
256
|
-
): string[] {
|
|
257
|
-
const counts = countReviewProgressTasks(state.tasks);
|
|
258
|
-
const spinner = getProgressSpinner(state);
|
|
259
|
-
const hint = `${theme.fg("muted", `(${formatDuration(Date.now() - state.startedAtMs)}, `)}${theme.fg(
|
|
260
|
-
"dim",
|
|
261
|
-
keyText("app.tools.expand"),
|
|
262
|
-
)}${theme.fg("muted", " to collapse)")}`;
|
|
263
|
-
const header = truncateToWidth(
|
|
264
|
-
`${theme.fg("accent", spinner)} reviewing ${counts.finished}/${counts.total} ${hint}`,
|
|
265
|
-
width,
|
|
266
|
-
theme.fg("muted", "…"),
|
|
267
|
-
);
|
|
268
|
-
const groupedTasks = groupReviewProgressTasks(state.tasks);
|
|
269
|
-
const fullRows = buildReviewProgressRows(groupedTasks, theme, width, "full", spinner);
|
|
270
|
-
const rows = fullRows.fits
|
|
271
|
-
? fullRows.lines
|
|
272
|
-
: buildReviewProgressRows(groupedTasks, theme, width, "short", spinner).lines;
|
|
273
|
-
return [header, ...rows, theme.fg("borderMuted", "─".repeat(Math.max(0, width)))];
|
|
276
|
+
const failedText = counts.failed > 0 ? ` · ${counts.failed} failed` : "";
|
|
277
|
+
return `${counts.finished}/${counts.total} complete${failedText}`;
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
function buildReviewProgressRows(
|
|
277
281
|
groupedTasks: Map<string, ReviewProgressTask[]>,
|
|
278
282
|
theme: ReviewTheme,
|
|
279
283
|
width: number,
|
|
280
|
-
labelStyle: "full" | "short",
|
|
281
284
|
spinner: string,
|
|
282
|
-
):
|
|
285
|
+
): string[] {
|
|
283
286
|
const focuses = getReviewProgressFocuses(groupedTasks);
|
|
284
287
|
const rowData = Array.from(groupedTasks, ([model, tasks]) => ({
|
|
285
288
|
model,
|
|
286
|
-
chips: focuses
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
task?.status ?? "running",
|
|
291
|
-
focus,
|
|
292
|
-
labelStyle,
|
|
293
|
-
spinner,
|
|
294
|
-
theme,
|
|
295
|
-
);
|
|
296
|
-
})
|
|
297
|
-
.join(" "),
|
|
289
|
+
chips: focuses.map((focus) => {
|
|
290
|
+
const task = tasks.find((candidate) => candidate.focus === focus);
|
|
291
|
+
return formatReviewProgressChip(task?.status ?? "running", focus, spinner, theme);
|
|
292
|
+
}),
|
|
298
293
|
}));
|
|
294
|
+
const rowWidth = Math.max(0, width - 4);
|
|
299
295
|
const maxModelWidth = Math.max(0, ...rowData.map((row) => visibleWidth(row.model)));
|
|
300
|
-
const maxChipWidth = Math.max(0, ...rowData.map((row) => visibleWidth(row.chips)));
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
296
|
+
const maxChipWidth = Math.max(0, ...rowData.map((row) => visibleWidth(row.chips.join(" "))));
|
|
297
|
+
const modelWidth = Math.min(maxModelWidth, Math.max(0, rowWidth - maxChipWidth - 2));
|
|
298
|
+
return rowData.flatMap((row) => {
|
|
299
|
+
if (modelWidth >= Math.min(16, maxModelWidth)) {
|
|
300
|
+
const model = padToWidth(truncateToWidth(row.model, modelWidth), modelWidth);
|
|
301
|
+
return [truncateToWidth(` ${theme.fg("dim", model)} ${row.chips.join(" ")}`, width)];
|
|
302
|
+
}
|
|
303
|
+
// Keep focus names and their statuses together when the matrix cannot fit on one line.
|
|
304
|
+
const lines = [
|
|
305
|
+
truncateToWidth(` ${theme.fg("dim", truncateToWidth(row.model, rowWidth))}`, width),
|
|
306
|
+
];
|
|
307
|
+
const chipWidth = Math.max(0, rowWidth - 2);
|
|
308
|
+
let line = "";
|
|
309
|
+
for (const chip of row.chips) {
|
|
310
|
+
if (line && visibleWidth(line) + 2 + visibleWidth(chip) > chipWidth) {
|
|
311
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
312
|
+
line = "";
|
|
313
|
+
}
|
|
314
|
+
line += (line ? " " : "") + truncateToWidth(chip, chipWidth);
|
|
315
|
+
}
|
|
316
|
+
if (line) lines.push(truncateToWidth(` ${line}`, width));
|
|
317
|
+
return lines;
|
|
312
318
|
});
|
|
313
|
-
|
|
314
|
-
return { fits: canFit, lines };
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
function getReviewProgressFocuses(groupedTasks: Map<string, ReviewProgressTask[]>): ReviewFocus[] {
|
|
@@ -327,18 +331,17 @@ function getReviewProgressFocuses(groupedTasks: Map<string, ReviewProgressTask[]
|
|
|
327
331
|
function formatReviewProgressChip(
|
|
328
332
|
status: ReviewProgressStatus,
|
|
329
333
|
focus: ReviewFocus,
|
|
330
|
-
labelStyle: "full" | "short",
|
|
331
334
|
spinner: string,
|
|
332
335
|
theme: ReviewTheme,
|
|
333
336
|
): string {
|
|
334
|
-
const label =
|
|
337
|
+
const label = theme.fg("text", focus);
|
|
335
338
|
switch (status) {
|
|
336
339
|
case "success":
|
|
337
340
|
return `${theme.fg("success", "✓")} ${label}`;
|
|
338
341
|
case "failure":
|
|
339
342
|
return `${theme.fg("error", "✕")} ${label}`;
|
|
340
343
|
case "running":
|
|
341
|
-
return `${spinner} ${label}`;
|
|
344
|
+
return `${theme.fg("accent", spinner)} ${label}`;
|
|
342
345
|
}
|
|
343
346
|
}
|
|
344
347
|
|
|
@@ -373,23 +376,6 @@ function getProgressSpinner(state: ReviewProgressState): string {
|
|
|
373
376
|
return STATUS_SPINNER_FRAMES[state.frame % STATUS_SPINNER_FRAMES.length];
|
|
374
377
|
}
|
|
375
378
|
|
|
376
|
-
function getShortReviewFocusLabel(focus: ReviewFocus): string {
|
|
377
|
-
switch (focus) {
|
|
378
|
-
case "general":
|
|
379
|
-
return "gen";
|
|
380
|
-
case "security":
|
|
381
|
-
return "sec";
|
|
382
|
-
case "reuse":
|
|
383
|
-
return "reuse";
|
|
384
|
-
case "quality":
|
|
385
|
-
return "qual";
|
|
386
|
-
case "testing":
|
|
387
|
-
return "test";
|
|
388
|
-
case "efficiency":
|
|
389
|
-
return "eff";
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
379
|
function padToWidth(value: string, width: number): string {
|
|
394
380
|
return `${value}${" ".repeat(Math.max(0, width - visibleWidth(value)))}`;
|
|
395
381
|
}
|
|
@@ -400,7 +386,7 @@ function priorityRank(priority: Priority): number {
|
|
|
400
386
|
|
|
401
387
|
function buildFocusPrompt(
|
|
402
388
|
focus: ReviewFocus,
|
|
403
|
-
|
|
389
|
+
scope: ResolvedScope,
|
|
404
390
|
projectGuidelines: string | null,
|
|
405
391
|
additionalContext: string | undefined,
|
|
406
392
|
): string {
|
|
@@ -408,10 +394,18 @@ function buildFocusPrompt(
|
|
|
408
394
|
const projectGuidelinesSection = buildProjectReviewGuidelinesSection(projectGuidelines);
|
|
409
395
|
|
|
410
396
|
const def = REVIEW_FOCUSES[focus];
|
|
397
|
+
const scopePolicy =
|
|
398
|
+
scope.kind === "folder"
|
|
399
|
+
? REVIEW_SNAPSHOT_SCOPE_PROMPT
|
|
400
|
+
: scope.kind === "custom"
|
|
401
|
+
? `Use the custom instructions to determine the review scope and whether it is a diff or snapshot review. Apply the corresponding default below unless those instructions override it.\n\n${REVIEW_DIFF_SCOPE_PROMPT}\n\n${REVIEW_SNAPSHOT_SCOPE_PROMPT}`
|
|
402
|
+
: REVIEW_DIFF_SCOPE_PROMPT;
|
|
411
403
|
|
|
412
404
|
return REVIEW_FOCUS_PROMPT.replace("{FOCUS_SUFFIX}", () => def.suffix)
|
|
413
405
|
.replace("{FOCUS_QUALIFIER}", () => def.qualifier)
|
|
414
|
-
.replace("{SCOPE_INSTRUCTIONS}", () =>
|
|
406
|
+
.replace("{SCOPE_INSTRUCTIONS}", () => buildScopeInstructions(scope))
|
|
407
|
+
.replace("{REVIEW_RUBRIC}", () => REVIEW_RUBRIC_PROMPT)
|
|
408
|
+
.replace("{SCOPE_POLICY}", () => scopePolicy)
|
|
415
409
|
.replace("{FOCUS_CONTEXT}", () => def.context)
|
|
416
410
|
.replace("{ADDITIONAL_CONTEXT_SECTION}", () => additionalContextSection)
|
|
417
411
|
.replace("{PROJECT_GUIDELINES_SECTION}", () => projectGuidelinesSection)
|
|
@@ -562,7 +556,7 @@ function createFocusTaskAttempt(
|
|
|
562
556
|
async function runFocusTaskOnce(
|
|
563
557
|
task: FocusTaskAttempt,
|
|
564
558
|
cwd: string,
|
|
565
|
-
|
|
559
|
+
signal: AbortSignal,
|
|
566
560
|
): Promise<FocusTaskResult> {
|
|
567
561
|
const modelLabel = buildFocusTaskAttemptModelLabel(task);
|
|
568
562
|
const modelArg = getFocusTaskAttemptModelArg(task);
|
|
@@ -588,7 +582,7 @@ async function runFocusTaskOnce(
|
|
|
588
582
|
prompt: task.prompt,
|
|
589
583
|
cwd,
|
|
590
584
|
timeoutMs: REVIEW_TASK_TIMEOUT_MS,
|
|
591
|
-
|
|
585
|
+
signal,
|
|
592
586
|
submitTool: SUBMIT_REVIEW_TOOL,
|
|
593
587
|
});
|
|
594
588
|
|
|
@@ -680,21 +674,19 @@ async function runFocusTaskOnce(
|
|
|
680
674
|
async function runFocusTaskWithRetry(
|
|
681
675
|
task: FocusTaskAttempt,
|
|
682
676
|
cwd: string,
|
|
683
|
-
|
|
677
|
+
signal: AbortSignal,
|
|
684
678
|
): Promise<FocusTaskResult> {
|
|
685
679
|
for (let attempt = 0; ; attempt += 1) {
|
|
686
|
-
const result = await runFocusTaskOnce(task, cwd,
|
|
680
|
+
const result = await runFocusTaskOnce(task, cwd, signal);
|
|
687
681
|
if (result.ok || attempt >= REVIEW_STARTUP_RETRY_DELAYS_MS.length) return result;
|
|
688
682
|
|
|
689
683
|
if (result.errorKind !== "lock_contention") return result;
|
|
690
|
-
|
|
691
|
-
return createCancelledFocusAttemptResult(task);
|
|
692
|
-
}
|
|
684
|
+
signal.throwIfAborted();
|
|
693
685
|
|
|
694
686
|
const baseDelayMs =
|
|
695
687
|
REVIEW_STARTUP_RETRY_DELAYS_MS[attempt] ??
|
|
696
688
|
REVIEW_STARTUP_RETRY_DELAYS_MS[REVIEW_STARTUP_RETRY_DELAYS_MS.length - 1];
|
|
697
|
-
await
|
|
689
|
+
await delay(withJitter(baseDelayMs), undefined, { signal });
|
|
698
690
|
}
|
|
699
691
|
}
|
|
700
692
|
|
|
@@ -722,7 +714,7 @@ async function runFocusTaskForProviderCandidate(
|
|
|
722
714
|
task: FocusTask,
|
|
723
715
|
providerCandidate: ResolvedReviewProviderCandidate,
|
|
724
716
|
cwd: string,
|
|
725
|
-
|
|
717
|
+
signal: AbortSignal,
|
|
726
718
|
): Promise<FocusTaskResult> {
|
|
727
719
|
const thinkingLevels = getFocusTaskThinkingLevels(task, providerCandidate);
|
|
728
720
|
const triedLevels: ReviewThinkingLevel[] = [];
|
|
@@ -736,7 +728,7 @@ async function runFocusTaskForProviderCandidate(
|
|
|
736
728
|
return createUnsupportedReasoningFocusAttemptResult(attempt, explicitThinkingSupportError);
|
|
737
729
|
}
|
|
738
730
|
|
|
739
|
-
const result = await runFocusTaskWithRetry(attempt, cwd,
|
|
731
|
+
const result = await runFocusTaskWithRetry(attempt, cwd, signal);
|
|
740
732
|
if (result.ok) return result;
|
|
741
733
|
|
|
742
734
|
lastResult = result;
|
|
@@ -761,9 +753,9 @@ async function runFocusTaskForProviderCandidate(
|
|
|
761
753
|
async function runFocusTask(
|
|
762
754
|
task: FocusTask,
|
|
763
755
|
cwd: string,
|
|
764
|
-
|
|
756
|
+
signal: AbortSignal,
|
|
765
757
|
): Promise<FocusTaskResult> {
|
|
766
|
-
if (
|
|
758
|
+
if (signal.aborted) {
|
|
767
759
|
return createCancelledFocusResult(task);
|
|
768
760
|
}
|
|
769
761
|
|
|
@@ -775,7 +767,7 @@ async function runFocusTask(
|
|
|
775
767
|
continue;
|
|
776
768
|
}
|
|
777
769
|
if (availability === "supported") {
|
|
778
|
-
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd,
|
|
770
|
+
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd, signal);
|
|
779
771
|
if (result.ok) return result;
|
|
780
772
|
lastResult = result;
|
|
781
773
|
if (
|
|
@@ -795,7 +787,7 @@ async function runFocusTask(
|
|
|
795
787
|
continue;
|
|
796
788
|
}
|
|
797
789
|
|
|
798
|
-
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd,
|
|
790
|
+
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd, signal);
|
|
799
791
|
if (result.ok) return result;
|
|
800
792
|
lastResult = result;
|
|
801
793
|
if (
|
|
@@ -814,10 +806,12 @@ async function runFocusTask(
|
|
|
814
806
|
resolveProbe = resolve;
|
|
815
807
|
rejectProbe = reject;
|
|
816
808
|
});
|
|
809
|
+
// The initiating task reports errors even when no sibling awaits this shared probe.
|
|
810
|
+
void probe.catch(() => {});
|
|
817
811
|
setProviderCandidateProbe(task.model, providerCandidate, probe);
|
|
818
812
|
|
|
819
813
|
try {
|
|
820
|
-
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd,
|
|
814
|
+
const result = await runFocusTaskForProviderCandidate(task, providerCandidate, cwd, signal);
|
|
821
815
|
lastResult = result;
|
|
822
816
|
const probedAvailability = getProviderCandidateAvailabilityFromResult(result);
|
|
823
817
|
setProviderCandidateAvailability(task.model, providerCandidate, probedAvailability);
|
|
@@ -839,19 +833,21 @@ async function runFocusTask(
|
|
|
839
833
|
}
|
|
840
834
|
|
|
841
835
|
async function runReviewDedupTask(options: {
|
|
836
|
+
pi: ExtensionAPI;
|
|
842
837
|
ctx: ExtensionCommandContext;
|
|
843
838
|
cwd: string;
|
|
844
839
|
findings: ReviewReportFinding[];
|
|
845
|
-
|
|
840
|
+
signal: AbortSignal;
|
|
846
841
|
}): Promise<ReviewDedupGroup[] | null> {
|
|
847
|
-
const { ctx, cwd, findings,
|
|
842
|
+
const { pi, ctx, cwd, findings, signal } = options;
|
|
848
843
|
if (findings.length <= 1) return [];
|
|
849
|
-
if (
|
|
844
|
+
if (signal.aborted) return null;
|
|
850
845
|
|
|
851
846
|
const model = selectReviewDedupModel(ctx);
|
|
852
847
|
if (!model) return null;
|
|
853
848
|
|
|
854
849
|
const taskResult = await withSpinner(
|
|
850
|
+
pi,
|
|
855
851
|
ctx,
|
|
856
852
|
() => `deduplicating ${findings.length} review findings`,
|
|
857
853
|
() =>
|
|
@@ -872,7 +868,7 @@ async function runReviewDedupTask(options: {
|
|
|
872
868
|
prompt: buildReviewDedupPrompt(findings),
|
|
873
869
|
cwd,
|
|
874
870
|
timeoutMs: REVIEW_TASK_TIMEOUT_MS,
|
|
875
|
-
|
|
871
|
+
signal,
|
|
876
872
|
}),
|
|
877
873
|
);
|
|
878
874
|
|
|
@@ -975,7 +971,7 @@ function parseReviewMessageDetails(value: unknown): ReviewMessageDetails | null
|
|
|
975
971
|
}
|
|
976
972
|
|
|
977
973
|
export function getLastMessageReviewDetails(ctx: ExtensionContext): ReviewMessageDetails | null {
|
|
978
|
-
const entry = ctx.sessionManager.getBranch().
|
|
974
|
+
const entry = ctx.sessionManager.getBranch().findLast((entry) => entry.type !== "usage");
|
|
979
975
|
if (!entry || entry.type !== "custom_message" || entry.customType !== "review") {
|
|
980
976
|
return null;
|
|
981
977
|
}
|
|
@@ -992,7 +988,6 @@ function buildReviewTasks(
|
|
|
992
988
|
models: ResolvedReviewModel[],
|
|
993
989
|
focuses: readonly ReviewFocus[],
|
|
994
990
|
): FocusTask[] {
|
|
995
|
-
const scopeInstructions = buildScopeInstructions(scope);
|
|
996
991
|
const tasks: FocusTask[] = [];
|
|
997
992
|
|
|
998
993
|
for (const model of models) {
|
|
@@ -1000,7 +995,7 @@ function buildReviewTasks(
|
|
|
1000
995
|
tasks.push({
|
|
1001
996
|
model,
|
|
1002
997
|
focus,
|
|
1003
|
-
prompt: buildFocusPrompt(focus,
|
|
998
|
+
prompt: buildFocusPrompt(focus, scope, guidelines, additionalContext),
|
|
1004
999
|
});
|
|
1005
1000
|
}
|
|
1006
1001
|
}
|
|
@@ -1012,12 +1007,14 @@ async function prepareReviewRun(
|
|
|
1012
1007
|
pi: ExtensionAPI,
|
|
1013
1008
|
ctx: ExtensionCommandContext,
|
|
1014
1009
|
request: ParsedRequest,
|
|
1010
|
+
signal: AbortSignal,
|
|
1015
1011
|
): Promise<{ ok: false; error: string } | { ok: true; data: PreparedReviewRun }> {
|
|
1016
|
-
|
|
1012
|
+
const workspace = { cwd: ctx.cwd, signal };
|
|
1013
|
+
if (!(await isGitRepo(workspace))) {
|
|
1017
1014
|
return { ok: false, error: "Not a git repository." };
|
|
1018
1015
|
}
|
|
1019
1016
|
|
|
1020
|
-
const resolved = await resolveScope(
|
|
1017
|
+
const resolved = await resolveScope(workspace, request.target, (message, type) =>
|
|
1021
1018
|
notify(ctx, message, type),
|
|
1022
1019
|
);
|
|
1023
1020
|
if (!resolved.scope) {
|
|
@@ -1027,10 +1024,10 @@ async function prepareReviewRun(
|
|
|
1027
1024
|
const scope = resolved.scope;
|
|
1028
1025
|
const includeUntracked = scope.kind === "working-tree" || scope.kind === "folder";
|
|
1029
1026
|
const scopeUntrackedFiles = scope.kind === "working-tree" ? scope.untrackedFiles : undefined;
|
|
1030
|
-
const [baselineFingerprint, guidelines, models] = await
|
|
1031
|
-
computeCurrentFingerprint(
|
|
1032
|
-
loadProjectReviewGuidelines(
|
|
1033
|
-
resolveModels(ctx, request.models, pi.getThinkingLevel()),
|
|
1027
|
+
const [baselineFingerprint, guidelines, models] = await joinAll([
|
|
1028
|
+
computeCurrentFingerprint(workspace, includeUntracked, scopeUntrackedFiles),
|
|
1029
|
+
loadProjectReviewGuidelines(workspace),
|
|
1030
|
+
resolveModels(ctx, request.models, pi.getThinkingLevel(), signal),
|
|
1034
1031
|
]);
|
|
1035
1032
|
|
|
1036
1033
|
return {
|
|
@@ -1052,18 +1049,19 @@ async function prepareReviewRun(
|
|
|
1052
1049
|
}
|
|
1053
1050
|
|
|
1054
1051
|
async function runFocusTasks(
|
|
1052
|
+
pi: ExtensionAPI,
|
|
1055
1053
|
ctx: ExtensionCommandContext,
|
|
1056
1054
|
cwd: string,
|
|
1057
1055
|
tasks: FocusTask[],
|
|
1058
|
-
|
|
1056
|
+
signal: AbortSignal,
|
|
1059
1057
|
): Promise<FocusTaskResult[]> {
|
|
1060
|
-
const progress = createReviewProgress(ctx, tasks);
|
|
1058
|
+
const progress = createReviewProgress(pi, ctx, tasks);
|
|
1061
1059
|
try {
|
|
1062
|
-
return await
|
|
1060
|
+
return await joinAll(
|
|
1063
1061
|
tasks.map(async (task) => {
|
|
1064
|
-
const result =
|
|
1062
|
+
const result = signal.aborted
|
|
1065
1063
|
? createCancelledFocusResult(task)
|
|
1066
|
-
: await runFocusTask(task, cwd,
|
|
1064
|
+
: await runFocusTask(task, cwd, signal);
|
|
1067
1065
|
progress.update(task, result);
|
|
1068
1066
|
return result;
|
|
1069
1067
|
}),
|
|
@@ -1124,10 +1122,11 @@ function applyReviewDedupGroups(
|
|
|
1124
1122
|
}
|
|
1125
1123
|
|
|
1126
1124
|
async function buildReviewFindings(
|
|
1125
|
+
pi: ExtensionAPI,
|
|
1127
1126
|
ctx: ExtensionCommandContext,
|
|
1128
1127
|
cwd: string,
|
|
1129
1128
|
successfulFocuses: Array<FocusTaskResult & { output: FocusOutput }>,
|
|
1130
|
-
|
|
1129
|
+
signal: AbortSignal,
|
|
1131
1130
|
): Promise<ReviewReportFinding[]> {
|
|
1132
1131
|
const findings = successfulFocuses.flatMap((focus) =>
|
|
1133
1132
|
focus.output.findings.map((finding) => ({
|
|
@@ -1143,10 +1142,11 @@ async function buildReviewFindings(
|
|
|
1143
1142
|
}
|
|
1144
1143
|
|
|
1145
1144
|
const dedupGroups = await runReviewDedupTask({
|
|
1145
|
+
pi,
|
|
1146
1146
|
ctx,
|
|
1147
1147
|
cwd,
|
|
1148
1148
|
findings,
|
|
1149
|
-
|
|
1149
|
+
signal,
|
|
1150
1150
|
});
|
|
1151
1151
|
return dedupGroups ? applyReviewDedupGroups(findings, dedupGroups) : findings;
|
|
1152
1152
|
}
|
|
@@ -1156,14 +1156,13 @@ export async function runReviewPipeline(
|
|
|
1156
1156
|
ctx: ExtensionCommandContext,
|
|
1157
1157
|
request: ParsedRequest,
|
|
1158
1158
|
source: ReviewRunSource,
|
|
1159
|
+
runtime: ReviewRuntime,
|
|
1159
1160
|
): Promise<ReviewRunResult> {
|
|
1160
1161
|
const startedAtMs = Date.now();
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
return withManagedReviewRun(pi, ctx, source, async (managed) => {
|
|
1162
|
+
return runtime.run(ctx, source, async (signal): Promise<ReviewRunResult> => {
|
|
1163
|
+
const prepared = await prepareReviewRun(pi, ctx, request, signal);
|
|
1164
|
+
signal.throwIfAborted();
|
|
1165
|
+
if (!prepared.ok) return prepared;
|
|
1167
1166
|
const { scope, includeUntracked, baselineFingerprint, models, tasks } = prepared.data;
|
|
1168
1167
|
if (source === "review") {
|
|
1169
1168
|
const modelsText = models
|
|
@@ -1172,11 +1171,8 @@ export async function runReviewPipeline(
|
|
|
1172
1171
|
notify(ctx, `Review focuses: ${request.focuses.join(", ")} · models: ${modelsText}.`, "info");
|
|
1173
1172
|
}
|
|
1174
1173
|
|
|
1175
|
-
const focusResults = await runFocusTasks(ctx, ctx.cwd, tasks,
|
|
1176
|
-
|
|
1177
|
-
managed.markCancelled();
|
|
1178
|
-
return { ok: false, error: REVIEW_CANCELLED_ERROR };
|
|
1179
|
-
}
|
|
1174
|
+
const focusResults = await runFocusTasks(pi, ctx, ctx.cwd, tasks, signal);
|
|
1175
|
+
signal.throwIfAborted();
|
|
1180
1176
|
|
|
1181
1177
|
const failedFocuses = focusResults.filter((focus) => !focus.ok);
|
|
1182
1178
|
const failedCount = failedFocuses.length;
|
|
@@ -1219,17 +1215,17 @@ export async function runReviewPipeline(
|
|
|
1219
1215
|
};
|
|
1220
1216
|
}
|
|
1221
1217
|
|
|
1222
|
-
const findings = await buildReviewFindings(ctx, ctx.cwd, successfulFocuses,
|
|
1218
|
+
const findings = await buildReviewFindings(pi, ctx, ctx.cwd, successfulFocuses, signal);
|
|
1223
1219
|
|
|
1224
|
-
const endingFingerprint = await computeCurrentFingerprint(
|
|
1220
|
+
const endingFingerprint = await computeCurrentFingerprint(
|
|
1221
|
+
{ cwd: ctx.cwd, signal },
|
|
1222
|
+
includeUntracked,
|
|
1223
|
+
);
|
|
1225
1224
|
const reviewStaleness = !fingerprintsEqual(baselineFingerprint, endingFingerprint)
|
|
1226
1225
|
? buildReviewStaleness(source)
|
|
1227
1226
|
: undefined;
|
|
1228
1227
|
|
|
1229
|
-
|
|
1230
|
-
managed.markCancelled();
|
|
1231
|
-
return { ok: false, error: REVIEW_CANCELLED_ERROR };
|
|
1232
|
-
}
|
|
1228
|
+
signal.throwIfAborted();
|
|
1233
1229
|
|
|
1234
1230
|
const reviewedScopeLine = buildReviewedScopeLine(scope, Date.now() - startedAtMs);
|
|
1235
1231
|
let findingsMarkdown = buildReviewFindingsMarkdown(
|
|
@@ -1299,7 +1295,6 @@ export async function runReviewPipeline(
|
|
|
1299
1295
|
notify(ctx, `Review completed: ${findings.length} finding(s).`, "info");
|
|
1300
1296
|
}
|
|
1301
1297
|
|
|
1302
|
-
managed.markSuccessful();
|
|
1303
1298
|
return { ok: true, details };
|
|
1304
1299
|
});
|
|
1305
1300
|
}
|