import { prompt } from "@oh-my-pi/pi-utils"; import annotationsTemplate from "./prompts/annotations.md" with { type: "text" }; import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" }; import type { CodeReviewAnnotation } from "@oh-my-pi/pi-tui/overlays/annotation-types"; import { getRecommendedReviewAgentCount, getReviewDiffPreview, type ReviewDiffSnapshot } from "./diff"; import type { ResolvedReviewTarget } from "./target"; const LARGE_DIFF_CHARACTER_LIMIT = 50_000; const LARGE_DIFF_FILE_LIMIT = 20; export interface FormatCodeReviewAnnotationsOptions { forReviewer: boolean; supplementalInstructions?: string; } type RenderedAnnotation = CodeReviewAnnotation & { pathLabel: string; lineLabel?: string; isLine: boolean; }; interface ReviewPromptFile { path: string; diffIndex: number; diffUrl?: string; linesAdded: number; linesRemoved: number; ext: string; hunksPreview: string; } function formatPathLabel(annotation: CodeReviewAnnotation): string { return annotation.occurrence > 1 ? `${annotation.path} (${annotation.occurrence})` : annotation.path; } function formatLineLabel(annotation: Extract): string { if (annotation.oldLine !== undefined && annotation.newLine !== undefined) { return `old ${annotation.oldLine}, new ${annotation.newLine}`; } if (annotation.newLine !== undefined) return `new ${annotation.newLine}`; if (annotation.oldLine !== undefined) return `old ${annotation.oldLine}`; return "hunk"; } function renderReviewPromptFile( file: ReviewDiffSnapshot["files"][number], previewLines: number, prDiffUrl?: string, prDiffQuery = "", ): ReviewPromptFile { return { path: file.path, diffIndex: file.diffIndex, diffUrl: prDiffUrl ? `${prDiffUrl}/${file.diffIndex}${prDiffQuery}` : undefined, linesAdded: file.linesAdded, linesRemoved: file.linesRemoved, ext: file.path.match(/\.([^.]+)$/)?.[1] ?? "", hunksPreview: previewLines > 0 ? getReviewDiffPreview(file.rawDiff, previewLines) : "", }; } /** Formats exact annotations for a reviewer prompt or editor paste. */ export function formatCodeReviewAnnotations( annotations: readonly CodeReviewAnnotation[], options: FormatCodeReviewAnnotationsOptions, ): string | undefined { const supplementalInstructions = options.supplementalInstructions?.trim(); if (annotations.length === 0 && !supplementalInstructions) return undefined; const renderedAnnotations: RenderedAnnotation[] = annotations.map(annotation => annotation.scope === "line" ? { ...annotation, pathLabel: formatPathLabel(annotation), lineLabel: formatLineLabel(annotation), isLine: true, } : { ...annotation, pathLabel: formatPathLabel(annotation), isLine: false, }, ); return prompt.render(annotationsTemplate, { forReviewer: options.forReviewer, annotations: renderedAnnotations, supplementalInstructions, }); } /** Build pasteable notes or a full review request from a frozen diff. */ export function buildCodeReviewFeedback( target: ResolvedReviewTarget, annotations: readonly CodeReviewAnnotation[], review: boolean, focus: string | undefined, ): string | undefined { const formatted = formatCodeReviewAnnotations(annotations, { forReviewer: review, supplementalInstructions: focus, }); return review ? buildReviewPrompt(target, formatted) : formatted; } function exceedsInlineDiffLimit(target: ResolvedReviewTarget): boolean { return target.rawDiff.length > LARGE_DIFF_CHARACTER_LIMIT || target.snapshot.files.length > LARGE_DIFF_FILE_LIMIT; } /** Throws when a supplied patch is too large to embed in a review request; other targets pass. */ export function assertReviewablePatchSize(target: ResolvedReviewTarget): void { if (target.kind === "patch" && exceedsInlineDiffLimit(target)) { throw new Error( `Supplied diff exceeds the review limit (${LARGE_DIFF_CHARACTER_LIMIT} characters or ${LARGE_DIFF_FILE_LIMIT} files); split it into smaller patches.`, ); } } /** Renders a review request from one frozen target snapshot. */ export function buildReviewPrompt(target: ResolvedReviewTarget, additionalInstructions?: string): string { assertReviewablePatchSize(target); const skipDiff = exceedsInlineDiffLimit(target); const linesPerFile = skipDiff ? Math.max(5, Math.floor(100 / target.snapshot.files.length)) : 0; const prDiffQuery = target.prDiffQuery ?? ""; const files = target.snapshot.files.map(file => renderReviewPromptFile(file, linesPerFile, target.prDiffUrl, prDiffQuery), ); const agentCount = getRecommendedReviewAgentCount(target.snapshot); return prompt.render(reviewRequestTemplate, { mode: target.mode, repository: target.repository, files, excluded: target.snapshot.excluded, totalAdded: target.snapshot.totalAdded, totalRemoved: target.snapshot.totalRemoved, agentCount, multiAgent: agentCount > 1, skipDiff, linesPerFile, rawDiff: target.rawDiff.trim(), diffInstruction: target.diffInstruction, contextInstruction: target.contextInstruction, suppliedPatch: target.kind === "patch", prDiffUrl: target.prDiffUrl, prDiffQuery, additionalInstructions: additionalInstructions?.trim(), }); }