Files
backnotprop__plannotator/packages/server/review.ts
T

296 lines
9.7 KiB
TypeScript
Raw Normal View History

/**
* Code Review Server
*
* Provides a server implementation for code review with git diff rendering.
* Follows the same patterns as the plan server.
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote/devcontainer mode
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/
import { isRemoteSession, getServerPort } from "./remote";
import { type DiffType, type GitContext, runGitDiff, getWorktreeDiffOptions } from "./git";
import { getRepoInfo } from "./repo";
import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, type OpencodeClient } from "./shared-handlers";
import { contentHash, deleteDraft } from "./draft";
feat: VS Code editor annotations + theme integration (#239) * feat: add shared EditorAnnotation type Single source of truth for the editor annotation interface, imported by both @plannotator/server and @plannotator/ui. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation server endpoints In-memory store with POST/GET/DELETE endpoints for editor annotations. The array lives in the handler closure and dies with the server session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation UI — polling hook, card, panel, and export - useEditorAnnotations hook with auto-disable polling (500ms interval) - EditorAnnotationCard with file path, code preview, and comment - AnnotationPanel conditionally renders editor annotation section - exportEditorAnnotations formats annotations for Claude feedback - App.tsx wires hook, includes in output memo and send feedback gate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code extension editor annotation command Cmd+Shift+. or right-click to capture selected text as an annotation. POSTs through the cookie proxy to the plannotator server. Adds amber left-border decorations on annotated lines, cleared on panel close. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: inline comment threads, lightbulb menu, and stronger decorations Replace showInputBox with VS Code CommentController for inline annotation threads anchored to selected code. Add CodeActionProvider for lightbulb discoverability. Stronger decoration styling with gutter icon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code theme integration for webview Bridge VS Code CSS variables to Plannotator's CSS variable system via postMessage between wrapper page and proxied iframe. Automatically adopts the active VS Code color theme (dark/light/custom) without touching any UI components or CSS files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate editor annotation polling behind VS Code detection Only poll /api/editor-annotations when running inside a VS Code webview (window.__PLANNOTATOR_VSCODE). Browser and shared URL users now have zero network cost from the editor annotations feature. Also fixes poll interval from 500ms to 2000ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: faster polling, prevent unnecessary re-renders, unify HTTP helper - Poll interval 2s → 500ms for snappier annotation pickup - Shallow equality check on annotation IDs prevents React re-renders when poll data is unchanged - Merge postToProxy/deleteFromProxy into single requestProxy function Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotations support in code review Wire existing editor annotation infrastructure into the review flow so VS Code users can annotate files outside the diff during code review. - Add editor annotation endpoints to review server (same 3-line pattern) - Call useEditorAnnotations hook in review App.tsx - Display editor annotations in ReviewPanel with "Editor" divider - Include editor annotations in feedback export and gating logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:25:02 -08:00
import { createEditorAnnotationHandler } from "./editor-annotations";
// Re-export utilities
export { isRemoteSession, getServerPort } from "./remote";
export { openBrowser } from "./browser";
export { type DiffType, type DiffOption, type GitContext } from "./git";
export { handleServerReady as handleReviewServerReady } from "./shared-handlers";
// --- Types ---
export interface ReviewServerOptions {
/** Raw git diff patch string */
rawPatch: string;
/** Git ref used for the diff (e.g., "HEAD", "main..HEAD", "--staged") */
gitRef: string;
/** Error message if git diff failed */
error?: string;
/** HTML content to serve for the UI */
htmlContent: string;
/** Origin identifier for UI customization */
origin?: "opencode" | "claude-code";
/** Current diff type being displayed */
diffType?: DiffType;
/** Git context with branch info and available diff options */
gitContext?: GitContext;
/** Whether URL sharing is enabled (default: true) */
sharingEnabled?: boolean;
/** Custom base URL for share links (default: https://share.plannotator.ai) */
shareBaseUrl?: string;
/** Called when server starts with the URL, remote status, and port */
onReady?: (url: string, isRemote: boolean, port: number) => void;
/** OpenCode client for querying available agents (OpenCode only) */
opencodeClient?: OpencodeClient;
}
export interface ReviewServerResult {
/** The port the server is running on */
port: number;
/** The full URL to access the server */
url: string;
/** Whether running in remote mode */
isRemote: boolean;
/** Wait for user feedback submission */
waitForDecision: () => Promise<{
feedback: string;
annotations: unknown[];
agentSwitch?: string;
}>;
/** Stop the server */
stop: () => void;
}
// --- Server Implementation ---
const MAX_RETRIES = 5;
const RETRY_DELAY_MS = 500;
/**
* Start the Code Review server
*
* Handles:
* - Remote detection and port configuration
* - API routes (/api/diff, /api/feedback)
* - Port conflict retries
*/
export async function startReviewServer(
options: ReviewServerOptions
): Promise<ReviewServerResult> {
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady } = options;
const draftKey = contentHash(options.rawPatch);
feat: VS Code editor annotations + theme integration (#239) * feat: add shared EditorAnnotation type Single source of truth for the editor annotation interface, imported by both @plannotator/server and @plannotator/ui. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation server endpoints In-memory store with POST/GET/DELETE endpoints for editor annotations. The array lives in the handler closure and dies with the server session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation UI — polling hook, card, panel, and export - useEditorAnnotations hook with auto-disable polling (500ms interval) - EditorAnnotationCard with file path, code preview, and comment - AnnotationPanel conditionally renders editor annotation section - exportEditorAnnotations formats annotations for Claude feedback - App.tsx wires hook, includes in output memo and send feedback gate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code extension editor annotation command Cmd+Shift+. or right-click to capture selected text as an annotation. POSTs through the cookie proxy to the plannotator server. Adds amber left-border decorations on annotated lines, cleared on panel close. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: inline comment threads, lightbulb menu, and stronger decorations Replace showInputBox with VS Code CommentController for inline annotation threads anchored to selected code. Add CodeActionProvider for lightbulb discoverability. Stronger decoration styling with gutter icon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code theme integration for webview Bridge VS Code CSS variables to Plannotator's CSS variable system via postMessage between wrapper page and proxied iframe. Automatically adopts the active VS Code color theme (dark/light/custom) without touching any UI components or CSS files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate editor annotation polling behind VS Code detection Only poll /api/editor-annotations when running inside a VS Code webview (window.__PLANNOTATOR_VSCODE). Browser and shared URL users now have zero network cost from the editor annotations feature. Also fixes poll interval from 500ms to 2000ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: faster polling, prevent unnecessary re-renders, unify HTTP helper - Poll interval 2s → 500ms for snappier annotation pickup - Shallow equality check on annotation IDs prevents React re-renders when poll data is unchanged - Merge postToProxy/deleteFromProxy into single requestProxy function Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotations support in code review Wire existing editor annotation infrastructure into the review flow so VS Code users can annotate files outside the diff during code review. - Add editor annotation endpoints to review server (same 3-line pattern) - Call useEditorAnnotations hook in review App.tsx - Display editor annotations in ReviewPanel with "Editor" divider - Include editor annotations in feedback export and gating logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:25:02 -08:00
const editorAnnotations = createEditorAnnotationHandler();
// Mutable state for diff switching
let currentPatch = options.rawPatch;
let currentGitRef = options.gitRef;
let currentDiffType: DiffType = options.diffType || "uncommitted";
let currentError = options.error;
const isRemote = isRemoteSession();
const configuredPort = getServerPort();
// Detect repo info (cached for this session)
const repoInfo = await getRepoInfo();
// Decision promise
let resolveDecision: (result: {
feedback: string;
annotations: unknown[];
agentSwitch?: string;
}) => void;
const decisionPromise = new Promise<{
feedback: string;
annotations: unknown[];
agentSwitch?: string;
}>((resolve) => {
resolveDecision = resolve;
});
// Start server with retry logic
let server: ReturnType<typeof Bun.serve> | null = null;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
server = Bun.serve({
port: configuredPort,
async fetch(req) {
const url = new URL(req.url);
// API: Get diff content
if (url.pathname === "/api/diff" && req.method === "GET") {
return Response.json({
rawPatch: currentPatch,
gitRef: currentGitRef,
origin,
diffType: currentDiffType,
gitContext,
sharingEnabled,
shareBaseUrl,
repoInfo,
...(currentError && { error: currentError }),
});
}
// API: Switch diff type
if (url.pathname === "/api/diff/switch" && req.method === "POST") {
try {
const body = (await req.json()) as { diffType: DiffType };
let newDiffType = body.diffType;
if (!newDiffType) {
return Response.json(
{ error: "Missing diffType" },
{ status: 400 }
);
}
const defaultBranch = gitContext?.defaultBranch || "main";
const previousDiffType = currentDiffType;
// Handle "back to main repo" sentinel
const isBackToMain = newDiffType === ("back-to-main" as DiffType);
if (isBackToMain) {
newDiffType = "uncommitted" as DiffType;
}
// Run the new diff
const result = await runGitDiff(newDiffType, defaultBranch);
// Update state
currentPatch = result.patch;
currentGitRef = result.label;
currentDiffType = newDiffType;
currentError = result.error;
// Build response — include diffOptions on worktree mode transitions
const response: Record<string, unknown> = {
rawPatch: currentPatch,
gitRef: currentGitRef,
diffType: currentDiffType,
...(currentError && { error: currentError }),
};
if (isBackToMain) {
// Exiting worktree mode: restore original dropdown options
response.diffOptions = gitContext?.diffOptions;
} else if (
newDiffType.startsWith("worktree:") &&
!previousDiffType.startsWith("worktree:")
) {
// Entering worktree mode: send worktree-scoped dropdown options
const wtPath = newDiffType.slice("worktree:".length).replace(/:(?:uncommitted|last-commit|branch)$/, "");
response.diffOptions = getWorktreeDiffOptions(wtPath, defaultBranch);
}
return Response.json(response);
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to switch diff";
return Response.json({ error: message }, { status: 500 });
}
}
// API: Serve images (local paths or temp uploads)
if (url.pathname === "/api/image") {
return handleImage(req);
}
// API: Upload image -> save to temp -> return path
if (url.pathname === "/api/upload" && req.method === "POST") {
return handleUpload(req);
}
// API: Get available agents (OpenCode only)
if (url.pathname === "/api/agents") {
return handleAgents(options.opencodeClient);
}
// API: Annotation draft persistence
if (url.pathname === "/api/draft") {
if (req.method === "POST") return handleDraftSave(req, draftKey);
if (req.method === "DELETE") return handleDraftDelete(draftKey);
return handleDraftLoad(draftKey);
}
feat: VS Code editor annotations + theme integration (#239) * feat: add shared EditorAnnotation type Single source of truth for the editor annotation interface, imported by both @plannotator/server and @plannotator/ui. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation server endpoints In-memory store with POST/GET/DELETE endpoints for editor annotations. The array lives in the handler closure and dies with the server session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotation UI — polling hook, card, panel, and export - useEditorAnnotations hook with auto-disable polling (500ms interval) - EditorAnnotationCard with file path, code preview, and comment - AnnotationPanel conditionally renders editor annotation section - exportEditorAnnotations formats annotations for Claude feedback - App.tsx wires hook, includes in output memo and send feedback gate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code extension editor annotation command Cmd+Shift+. or right-click to capture selected text as an annotation. POSTs through the cookie proxy to the plannotator server. Adds amber left-border decorations on annotated lines, cleared on panel close. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: inline comment threads, lightbulb menu, and stronger decorations Replace showInputBox with VS Code CommentController for inline annotation threads anchored to selected code. Add CodeActionProvider for lightbulb discoverability. Stronger decoration styling with gutter icon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: VS Code theme integration for webview Bridge VS Code CSS variables to Plannotator's CSS variable system via postMessage between wrapper page and proxied iframe. Automatically adopts the active VS Code color theme (dark/light/custom) without touching any UI components or CSS files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate editor annotation polling behind VS Code detection Only poll /api/editor-annotations when running inside a VS Code webview (window.__PLANNOTATOR_VSCODE). Browser and shared URL users now have zero network cost from the editor annotations feature. Also fixes poll interval from 500ms to 2000ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: faster polling, prevent unnecessary re-renders, unify HTTP helper - Poll interval 2s → 500ms for snappier annotation pickup - Shallow equality check on annotation IDs prevents React re-renders when poll data is unchanged - Merge postToProxy/deleteFromProxy into single requestProxy function Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: editor annotations support in code review Wire existing editor annotation infrastructure into the review flow so VS Code users can annotate files outside the diff during code review. - Add editor annotation endpoints to review server (same 3-line pattern) - Call useEditorAnnotations hook in review App.tsx - Display editor annotations in ReviewPanel with "Editor" divider - Include editor annotations in feedback export and gating logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:25:02 -08:00
// API: Editor annotations (VS Code extension)
const editorResponse = await editorAnnotations.handle(req, url);
if (editorResponse) return editorResponse;
// API: Submit review feedback
if (url.pathname === "/api/feedback" && req.method === "POST") {
try {
const body = (await req.json()) as {
feedback: string;
annotations: unknown[];
agentSwitch?: string;
};
deleteDraft(draftKey);
resolveDecision({
feedback: body.feedback || "",
annotations: body.annotations || [],
agentSwitch: body.agentSwitch,
});
return Response.json({ ok: true });
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to process feedback";
return Response.json({ error: message }, { status: 500 });
}
}
// Serve embedded HTML for all other routes (SPA)
return new Response(htmlContent, {
headers: { "Content-Type": "text/html" },
});
},
});
break; // Success, exit retry loop
} catch (err: unknown) {
const isAddressInUse =
err instanceof Error && err.message.includes("EADDRINUSE");
if (isAddressInUse && attempt < MAX_RETRIES) {
await Bun.sleep(RETRY_DELAY_MS);
continue;
}
if (isAddressInUse) {
const hint = isRemote ? " (set PLANNOTATOR_PORT to use different port)" : "";
throw new Error(`Port ${configuredPort} in use after ${MAX_RETRIES} retries${hint}`);
}
throw err;
}
}
if (!server) {
throw new Error("Failed to start server");
}
const serverUrl = `http://localhost:${server.port}`;
// Notify caller that server is ready
if (onReady) {
onReady(serverUrl, isRemote, server.port);
}
return {
port: server.port,
url: serverUrl,
isRemote,
waitForDecision: () => decisionPromise,
stop: () => server.stop(),
};
}