2026-01-12 19:36:09 -08:00
|
|
|
/**
|
|
|
|
|
* 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";
|
2026-03-07 10:06:55 -08:00
|
|
|
import { type DiffType, type GitContext, runGitDiff, getWorktreeDiffOptions } from "./git";
|
2026-01-28 10:29:52 -08:00
|
|
|
import { getRepoInfo } from "./repo";
|
2026-03-04 10:12:58 -08:00
|
|
|
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";
|
2026-01-12 19:36:09 -08:00
|
|
|
|
|
|
|
|
// Re-export utilities
|
|
|
|
|
export { isRemoteSession, getServerPort } from "./remote";
|
|
|
|
|
export { openBrowser } from "./browser";
|
|
|
|
|
export { type DiffType, type DiffOption, type GitContext } from "./git";
|
2026-03-03 23:17:16 -08:00
|
|
|
export { handleServerReady as handleReviewServerReady } from "./shared-handlers";
|
2026-01-12 19:36:09 -08:00
|
|
|
|
|
|
|
|
// --- 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;
|
2026-03-02 15:26:07 +09:00
|
|
|
/** Error message if git diff failed */
|
|
|
|
|
error?: string;
|
2026-01-12 19:36:09 -08:00
|
|
|
/** 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;
|
2026-01-14 13:12:37 -08:00
|
|
|
/** Whether URL sharing is enabled (default: true) */
|
|
|
|
|
sharingEnabled?: boolean;
|
2026-02-12 15:07:43 -08:00
|
|
|
/** Custom base URL for share links (default: https://share.plannotator.ai) */
|
|
|
|
|
shareBaseUrl?: string;
|
2026-01-12 19:36:09 -08:00
|
|
|
/** Called when server starts with the URL, remote status, and port */
|
|
|
|
|
onReady?: (url: string, isRemote: boolean, port: number) => void;
|
2026-01-19 11:07:22 -08:00
|
|
|
/** OpenCode client for querying available agents (OpenCode only) */
|
2026-03-03 23:17:16 -08:00
|
|
|
opencodeClient?: OpencodeClient;
|
2026-01-12 19:36:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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> {
|
2026-02-12 15:07:43 -08:00
|
|
|
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady } = options;
|
2026-01-12 19:36:09 -08:00
|
|
|
|
2026-03-04 10:12:58 -08:00
|
|
|
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();
|
2026-03-04 10:12:58 -08:00
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
// Mutable state for diff switching
|
|
|
|
|
let currentPatch = options.rawPatch;
|
|
|
|
|
let currentGitRef = options.gitRef;
|
|
|
|
|
let currentDiffType: DiffType = options.diffType || "uncommitted";
|
2026-03-02 15:26:07 +09:00
|
|
|
let currentError = options.error;
|
2026-01-12 19:36:09 -08:00
|
|
|
|
|
|
|
|
const isRemote = isRemoteSession();
|
|
|
|
|
const configuredPort = getServerPort();
|
|
|
|
|
|
2026-01-28 10:29:52 -08:00
|
|
|
// Detect repo info (cached for this session)
|
|
|
|
|
const repoInfo = await getRepoInfo();
|
|
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
// 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,
|
2026-01-14 13:12:37 -08:00
|
|
|
sharingEnabled,
|
2026-02-12 15:07:43 -08:00
|
|
|
shareBaseUrl,
|
2026-01-28 10:29:52 -08:00
|
|
|
repoInfo,
|
2026-03-02 15:26:07 +09:00
|
|
|
...(currentError && { error: currentError }),
|
2026-01-12 19:36:09 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API: Switch diff type
|
|
|
|
|
if (url.pathname === "/api/diff/switch" && req.method === "POST") {
|
|
|
|
|
try {
|
|
|
|
|
const body = (await req.json()) as { diffType: DiffType };
|
2026-03-07 10:06:55 -08:00
|
|
|
let newDiffType = body.diffType;
|
2026-01-12 19:36:09 -08:00
|
|
|
|
|
|
|
|
if (!newDiffType) {
|
|
|
|
|
return Response.json(
|
|
|
|
|
{ error: "Missing diffType" },
|
|
|
|
|
{ status: 400 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultBranch = gitContext?.defaultBranch || "main";
|
2026-03-07 10:06:55 -08:00
|
|
|
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
|
2026-01-12 19:36:09 -08:00
|
|
|
const result = await runGitDiff(newDiffType, defaultBranch);
|
|
|
|
|
|
|
|
|
|
// Update state
|
|
|
|
|
currentPatch = result.patch;
|
|
|
|
|
currentGitRef = result.label;
|
|
|
|
|
currentDiffType = newDiffType;
|
2026-03-02 15:26:07 +09:00
|
|
|
currentError = result.error;
|
2026-01-12 19:36:09 -08:00
|
|
|
|
2026-03-07 10:06:55 -08:00
|
|
|
// Build response — include diffOptions on worktree mode transitions
|
|
|
|
|
const response: Record<string, unknown> = {
|
2026-01-12 19:36:09 -08:00
|
|
|
rawPatch: currentPatch,
|
|
|
|
|
gitRef: currentGitRef,
|
|
|
|
|
diffType: currentDiffType,
|
2026-03-02 15:26:07 +09:00
|
|
|
...(currentError && { error: currentError }),
|
2026-03-07 10:06:55 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-12 19:36:09 -08:00
|
|
|
} 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") {
|
2026-03-03 23:17:16 -08:00
|
|
|
return handleImage(req);
|
2026-01-12 19:36:09 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// API: Upload image -> save to temp -> return path
|
|
|
|
|
if (url.pathname === "/api/upload" && req.method === "POST") {
|
2026-03-03 23:17:16 -08:00
|
|
|
return handleUpload(req);
|
2026-01-12 19:36:09 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 11:07:22 -08:00
|
|
|
// API: Get available agents (OpenCode only)
|
|
|
|
|
if (url.pathname === "/api/agents") {
|
2026-03-03 23:17:16 -08:00
|
|
|
return handleAgents(options.opencodeClient);
|
2026-01-19 11:07:22 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 10:12:58 -08:00
|
|
|
// 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;
|
|
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
// 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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 10:12:58 -08:00
|
|
|
deleteDraft(draftKey);
|
2026-01-12 19:36:09 -08:00
|
|
|
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(),
|
|
|
|
|
};
|
|
|
|
|
}
|