mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat: PR review support via GitHub URL (#324)
* feat(review): add runtime-agnostic PR provider Introduces `packages/shared/pr-provider.ts` with a `PRRuntime` interface (same pattern as ReviewGitRuntime in review-core.ts) and GitHub PR operations: URL parsing, auth check, diff/metadata fetching, and file content retrieval via `gh` CLI. `packages/server/pr.ts` is the Bun wrapper that pre-binds the runtime, matching the git.ts pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(review): add PR mode to review server When `prMetadata` is provided to `startReviewServer`, the server enters PR mode: `/api/diff` includes PR metadata and omits gitContext, `/api/diff/switch` and `/api/git-add` return 400 (not applicable), and `/api/file-content` fetches from GitHub API using base/head SHAs instead of local git. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(review): PR review flow for Claude Code and OpenCode Detects URL argument in `/plannotator-review` command. When a GitHub PR URL is provided, fetches diff and metadata via `gh` CLI and starts the review server in PR mode. Local review mode is unchanged when no URL is passed. Updates slash command definitions to pass $ARGUMENTS through. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(review): PR metadata display in review UI Shows "PR Review" badge, PR title with link, and owner/repo in the header when reviewing a pull request. Diff switcher and staging controls auto-hide since gitContext is omitted in PR mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(review): contextualized PR feedback for agent In PR mode, the feedback markdown now includes PR metadata (repo, number, title, branches, URL) so the agent has full context about the remote PR being reviewed. Removes the aggressive "address all of them" instruction in PR mode since the content is self-explanatory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(review): extract exportReviewFeedback + add tests Moves the pure feedback construction function from App.tsx to utils/exportFeedback.ts so it can be unit tested. Drops the unused `files` parameter. 11 tests covering local/PR headers, annotation grouping, sorting, file-scope ordering, and suggested code rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(review): strengthen PR/local boundary tests Replaces shallow header checks with comprehensive boundary assertions: local mode must never contain PR-specific content (repo, URL, branches), PR mode must include all context fields and exclude the generic header. Covers null/undefined prMetadata edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(review): remove redundant empty-annotations test Covered by the "no annotations: returns generic empty regardless of prMetadata" test which checks all three cases (no arg, null, PR mode). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(review): guard fetchPR, fix UTF-8 decoding, correct useCallback deps - Wrap fetchPR() in try/catch in both hook and OpenCode entry points so network/auth errors show a clean message instead of a stack trace - Replace atob() with Buffer.from() for UTF-8 correct base64 decoding of PR file content from GitHub API - Fix stale useCallback deps in handleCopyFeedback (files → prMetadata) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(install): add $ARGUMENTS to review command for PR URL support Install scripts were writing the review slash command without $ARGUMENTS, so PR URLs passed to /plannotator-review were silently dropped. Also switches PS1 heredoc to single-quoted to prevent $ARGUMENTS from being expanded as a PowerShell variable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add PR review support to docs and READMEs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,11 +15,13 @@ import { getRepoInfo } from "./repo";
|
||||
import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, handleFavicon, type OpencodeClient } from "./shared-handlers";
|
||||
import { contentHash, deleteDraft } from "./draft";
|
||||
import { createEditorAnnotationHandler } from "./editor-annotations";
|
||||
import { type PRMetadata, fetchPRFileContent } from "./pr";
|
||||
|
||||
// Re-export utilities
|
||||
export { isRemoteSession, getServerPort } from "./remote";
|
||||
export { openBrowser } from "./browser";
|
||||
export { type DiffType, type DiffOption, type GitContext, type WorktreeInfo } from "./git";
|
||||
export { type PRMetadata } from "./pr";
|
||||
export { handleServerReady as handleReviewServerReady } from "./shared-handlers";
|
||||
|
||||
// --- Types ---
|
||||
@@ -47,6 +49,8 @@ export interface ReviewServerOptions {
|
||||
onReady?: (url: string, isRemote: boolean, port: number) => void;
|
||||
/** OpenCode client for querying available agents (OpenCode only) */
|
||||
opencodeClient?: OpencodeClient;
|
||||
/** PR metadata when reviewing a pull request (PR mode) */
|
||||
prMetadata?: PRMetadata;
|
||||
}
|
||||
|
||||
export interface ReviewServerResult {
|
||||
@@ -83,8 +87,9 @@ const RETRY_DELAY_MS = 500;
|
||||
export async function startReviewServer(
|
||||
options: ReviewServerOptions
|
||||
): Promise<ReviewServerResult> {
|
||||
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady } = options;
|
||||
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady, prMetadata } = options;
|
||||
|
||||
const isPRMode = !!prMetadata;
|
||||
const draftKey = contentHash(options.rawPatch);
|
||||
const editorAnnotations = createEditorAnnotationHandler();
|
||||
|
||||
@@ -98,7 +103,10 @@ export async function startReviewServer(
|
||||
const configuredPort = getServerPort();
|
||||
|
||||
// Detect repo info (cached for this session)
|
||||
const repoInfo = await getRepoInfo();
|
||||
// In PR mode, derive from metadata instead of local git
|
||||
const repoInfo = isPRMode
|
||||
? { display: `${prMetadata.owner}/${prMetadata.repo}`, branch: `PR #${prMetadata.number}` }
|
||||
: await getRepoInfo();
|
||||
|
||||
// Decision promise
|
||||
let resolveDecision: (result: {
|
||||
@@ -133,17 +141,24 @@ export async function startReviewServer(
|
||||
rawPatch: currentPatch,
|
||||
gitRef: currentGitRef,
|
||||
origin,
|
||||
diffType: currentDiffType,
|
||||
gitContext,
|
||||
diffType: isPRMode ? undefined : currentDiffType,
|
||||
gitContext: isPRMode ? undefined : gitContext,
|
||||
sharingEnabled,
|
||||
shareBaseUrl,
|
||||
repoInfo,
|
||||
...(isPRMode && { prMetadata }),
|
||||
...(currentError && { error: currentError }),
|
||||
});
|
||||
}
|
||||
|
||||
// API: Switch diff type
|
||||
// API: Switch diff type (disabled in PR mode)
|
||||
if (url.pathname === "/api/diff/switch" && req.method === "POST") {
|
||||
if (isPRMode) {
|
||||
return Response.json(
|
||||
{ error: "Not available for PR reviews" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const body = (await req.json()) as { diffType: DiffType };
|
||||
let newDiffType = body.diffType;
|
||||
@@ -195,6 +210,17 @@ export async function startReviewServer(
|
||||
return Response.json({ error: "Invalid path" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
if (isPRMode) {
|
||||
// Fetch file content from GitHub API using base/head SHAs
|
||||
const prRef = { platform: prMetadata.platform, owner: prMetadata.owner, repo: prMetadata.repo, number: prMetadata.number } as const;
|
||||
const [oldContent, newContent] = await Promise.all([
|
||||
fetchPRFileContent(prRef, prMetadata.baseSha, oldPath || filePath),
|
||||
fetchPRFileContent(prRef, prMetadata.headSha, filePath),
|
||||
]);
|
||||
return Response.json({ oldContent, newContent });
|
||||
}
|
||||
|
||||
const defaultBranch = gitContext?.defaultBranch || "main";
|
||||
const defaultCwd = gitContext?.cwd;
|
||||
const result = await getFileContentsForDiff(
|
||||
@@ -207,8 +233,14 @@ export async function startReviewServer(
|
||||
return Response.json(result);
|
||||
}
|
||||
|
||||
// API: Git add / reset (stage / unstage) a file
|
||||
// API: Git add / reset (stage / unstage) a file (disabled in PR mode)
|
||||
if (url.pathname === "/api/git-add" && req.method === "POST") {
|
||||
if (isPRMode) {
|
||||
return Response.json(
|
||||
{ error: "Not available for PR reviews" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const body = (await req.json()) as { filePath: string; undo?: boolean };
|
||||
if (!body.filePath) {
|
||||
|
||||
Reference in New Issue
Block a user