Files
Michael Ramos cb4de46636 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

77 lines
2.5 KiB
TypeScript

/**
* Editor Annotations — ephemeral in-memory store for VS Code editor selections.
*
* The VS Code extension POSTs annotations from the editor; the webview app
* polls to pick them up. The array lives in this closure and dies when the
* server stops. No disk persistence.
*/
import type { EditorAnnotation } from "@plannotator/shared/types";
export type { EditorAnnotation };
export interface EditorAnnotationHandler {
handle: (req: Request, url: URL) => Promise<Response | null>;
}
export function createEditorAnnotationHandler(): EditorAnnotationHandler {
const annotations: EditorAnnotation[] = [];
return {
async handle(req: Request, url: URL): Promise<Response | null> {
// GET /api/editor-annotations — return all
if (url.pathname === "/api/editor-annotations" && req.method === "GET") {
return Response.json({ annotations });
}
// POST /api/editor-annotation — add one
if (url.pathname === "/api/editor-annotation" && req.method === "POST") {
try {
const body = (await req.json()) as {
filePath?: string;
selectedText?: string;
lineStart?: number;
lineEnd?: number;
comment?: string;
};
if (!body.filePath || !body.selectedText || !body.lineStart || !body.lineEnd) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
const annotation: EditorAnnotation = {
id: crypto.randomUUID(),
filePath: body.filePath,
selectedText: body.selectedText,
lineStart: body.lineStart,
lineEnd: body.lineEnd,
comment: body.comment,
createdAt: Date.now(),
};
annotations.push(annotation);
return Response.json({ id: annotation.id });
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
}
// DELETE /api/editor-annotation?id=xxx — remove one
if (url.pathname === "/api/editor-annotation" && req.method === "DELETE") {
const id = url.searchParams.get("id");
if (!id) {
return Response.json({ error: "Missing id parameter" }, { status: 400 });
}
const idx = annotations.findIndex((a) => a.id === id);
if (idx !== -1) {
annotations.splice(idx, 1);
}
return Response.json({ ok: true });
}
// Not handled
return null;
},
};
}