import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { parseArguments } from "../src/arguments.ts"; import { publishReview, resolvePullRequest, verifyLocalCheckout, } from "../src/github.ts"; import { resolveInstructions } from "../src/instructions.ts"; import { detectReportLanguage, type ReportLanguage } from "../src/language.ts"; import { runReview } from "../src/orchestrator.ts"; import { runPiAgent } from "../src/pi-process.ts"; import { ReviewProgressPresenter } from "../src/progress.ts"; import { buildReviewPayload } from "../src/publication.ts"; import { renderReport } from "../src/report.ts"; import { writeSnapshot } from "../src/snapshot.ts"; import { THINKING_LEVELS, type ThinkingLevel } from "../src/types.ts"; function display(pi: ExtensionAPI, content: string): void { pi.sendMessage( { customType: "pi-code-review", content, display: true }, { triggerTurn: false }, ); } export default function codeReviewExtension(pi: ExtensionAPI): void { pi.registerCommand("code-review", { description: "Review a GitHub pull request with independent Pi agents", handler: async (args, ctx) => { let progress: ReviewProgressPresenter | undefined; let reportLanguage: ReportLanguage = "en"; try { const options = parseArguments(args); if (!ctx.model) throw new Error("No active Pi model is selected"); const inheritedThinking = pi.getThinkingLevel(); if (!THINKING_LEVELS.includes(inheritedThinking as ThinkingLevel)) { throw new Error( `Unsupported active thinking level: ${inheritedThinking}`, ); } const selection = { model: options.model ?? `${ctx.model.provider}/${ctx.model.id}`, thinking: options.thinking ?? (inheritedThinking as ThinkingLevel), }; progress = new ReviewProgressPresenter(ctx.ui, (content) => display(pi, content), ); progress.update("Resolving pull request"); const resolved = await resolvePullRequest(options.target, { cwd: ctx.cwd, signal: ctx.signal, }); const { snapshot } = resolved; progress.milestone("Pull request resolved", [ `PR #${snapshot.number}: ${snapshot.title}`, `${snapshot.files.length} changed file${snapshot.files.length === 1 ? "" : "s"}; ${snapshot.linkedIssues.length} linked issue${snapshot.linkedIssues.length === 1 ? "" : "s"}.`, ]); if (snapshot.state !== "OPEN" || snapshot.isDraft) { display( pi, `## Code review skipped\n\nPull request is ${snapshot.isDraft ? "a draft" : snapshot.state.toLowerCase()}.`, ); return; } if (options.comment && snapshot.existingReviewUrl) { display( pi, `## Code review skipped\n\nThis head SHA was already reviewed: ${snapshot.existingReviewUrl}`, ); return; } progress.update("Preparing review snapshot"); await verifyLocalCheckout( resolved.repositoryRoot, snapshot.headSha, undefined, ctx.signal, ); const instructions = await resolveInstructions( resolved.repositoryRoot, snapshot.files, ); reportLanguage = detectReportLanguage(instructions); const files = await writeSnapshot(snapshot, instructions); try { const result = await runReview({ snapshot, manifestPath: files.manifestPath, selection, signal: ctx.signal, onProgress: (event) => progress?.handle(event), execute: (execution) => runPiAgent({ ...execution, cwd: resolved.repositoryRoot, }), }); display(pi, renderReport(result, reportLanguage)); if (!options.comment) return; if (result.status !== "complete") { ctx.ui.notify("Incomplete review was not published", "warning"); return; } progress.update("Publishing GitHub review"); await publishReview( snapshot.owner, snapshot.repo, snapshot.number, buildReviewPayload(result, reportLanguage), { cwd: resolved.repositoryRoot, signal: ctx.signal }, ); ctx.ui.notify("GitHub review published", "info"); } finally { await files.dispose(); } } catch (error) { ctx.ui.notify( error instanceof Error ? error.message : "Code review failed", "error", ); } finally { progress?.dispose(); } }, }); }