From bfe036920ba70542dedaef44cb35559843adf16f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 01:00:30 +0000 Subject: [PATCH 01/10] Add feature plan for VS Code diff viewer button Scoping document for a new PlanDiffViewer button that opens the plan diff in VS Code's native diff viewer via `code --diff`. https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW --- plan.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..56fb373 --- /dev/null +++ b/plan.md @@ -0,0 +1,87 @@ +# Feature: "Open in VS Code Diff" Button + +## Overview + +Add a button to the PlanDiffViewer toolbar that opens the current plan diff in VS Code's native side-by-side diff viewer. When clicked, the server writes both versions (base and current) to temp files and invokes `code --diff `. + +## Context + +The PlanDiffViewer already shows diffs in two modes (rendered + raw). Adding a VS Code diff option gives power users access to VS Code's mature diff features: inline editing, word-level highlighting, minimap, search, etc. + +## Changes + +### 1. New server endpoint: `POST /api/plan/vscode-diff` + +**File:** `packages/server/index.ts` + +Add a new endpoint that: +- Accepts `{ basePlan: string, currentPlan: string, baseVersion: number }` in the request body +- Writes both plans to temp files in `/tmp/plannotator/`: + - `/tmp/plannotator/plan-v{N}.md` (base version) + - `/tmp/plannotator/plan-current.md` (current version) +- Spawns `code --diff ` via `Bun.spawn()` +- Returns `{ ok: true }` on success, or `{ error: string }` on failure +- If `code` CLI isn't found, returns a 400 with a helpful error message suggesting `Shell Command: Install 'code' command in PATH` + +The endpoint reuses the existing `UPLOAD_DIR` (`/tmp/plannotator/`) directory pattern already used for image uploads. + +### 2. UI button in PlanDiffViewer toolbar + +**File:** `packages/ui/components/plan-diff/PlanDiffViewer.tsx` + +Add a "VS Code" button in the diff mode switcher row (next to `PlanDiffModeSwitcher` and the version label). The button: +- Has the VS Code icon (simple inline SVG) and text "VS Code" (hidden on mobile, icon-only) +- Calls `POST /api/plan/vscode-diff` with the base plan + current plan +- Shows a brief loading state while the request is in flight +- On error, displays a toast/inline message + +**File:** `packages/ui/components/plan-diff/PlanDiffViewer.tsx` (props update) + +New props needed: +- `currentPlan: string` — the current plan markdown +- `basePlan: string` — the base version markdown +- `baseVersion: number` — the base version number (for temp file naming) + +These are already available in the parent (`App.tsx`) via the `usePlanDiff` hook (`planDiff.diffBasePlan`, `markdown`, `planDiff.diffBaseVersion`). + +### 3. Wire up in App.tsx + +**File:** `packages/editor/App.tsx` + +Pass the three new props to `PlanDiffViewer`: +```tsx + +``` + +### 4. Build and verify + +Run `bun run build:hook` to bundle the updated UI into the single-file HTML served by the hook server. + +## Design Decisions + +1. **Server-side `code --diff` invocation:** The UI runs in a browser but needs to execute a shell command. The Bun server already has full system access (it opens the browser, reads files, etc.), so adding a shell spawn is consistent with the architecture. + +2. **Temp files instead of piping:** VS Code `--diff` requires two file paths. Temp files in `/tmp/plannotator/` are the simplest approach and align with the existing image upload pattern. Files are overwritten on each click (no accumulation). + +3. **No new dependencies:** Uses `Bun.spawn()` for process execution and existing temp directory patterns. No new npm packages needed. + +4. **Graceful degradation:** If VS Code CLI isn't available, the error message tells the user how to install it. The button is always visible (not feature-detected) since the server environment may differ from what the browser can detect. + +## Files Modified + +| File | Change | +|------|--------| +| `packages/server/index.ts` | Add `POST /api/plan/vscode-diff` endpoint | +| `packages/ui/components/plan-diff/PlanDiffViewer.tsx` | Add VS Code button, new props | +| `packages/editor/App.tsx` | Pass new props to PlanDiffViewer | + +## Out of Scope + +- Opening in other editors (Cursor, Zed, etc.) — could be a follow-up with `PLANNOTATOR_EDITOR` env var +- Detecting whether VS Code is installed before showing the button +- Adding this to the code review diff viewer (`review-editor`) — separate feature From eaf4f78432999fc60d09d5152d530e4266823eff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 01:04:22 +0000 Subject: [PATCH 02/10] feat: add "Open in VS Code" button to plan diff viewer Add a button in the PlanDiffViewer toolbar that opens the current plan diff in VS Code's native side-by-side diff viewer. The server writes both versions to temp files and spawns `code --diff`. - New POST /api/plan/vscode-diff endpoint in packages/server/index.ts - VS Code button with loading/error states in PlanDiffViewer.tsx - Wire currentPlan/basePlan/baseVersion props from App.tsx https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW --- packages/editor/App.tsx | 3 + packages/server/index.ts | 52 +++++++++++++++ .../components/plan-diff/PlanDiffViewer.tsx | 64 ++++++++++++++++++- 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 4db80ac..ad009fa 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -1170,6 +1170,9 @@ const App: React.FC = () => { onPlanDiffToggle={() => setIsPlanDiffActive(false)} repoInfo={repoInfo} baseVersionLabel={planDiff.diffBaseVersion != null ? `v${planDiff.diffBaseVersion}` : undefined} + currentPlan={markdown} + basePlan={planDiff.diffBasePlan ?? undefined} + baseVersion={planDiff.diffBaseVersion ?? undefined} /> ) : ( void; repoInfo?: { display: string; branch?: string } | null; baseVersionLabel?: string; + currentPlan?: string; + basePlan?: string; + baseVersion?: number; } export const PlanDiffViewer: React.FC = ({ @@ -34,7 +37,36 @@ export const PlanDiffViewer: React.FC = ({ onPlanDiffToggle, repoInfo, baseVersionLabel, + currentPlan, + basePlan, + baseVersion, }) => { + const [vscodeDiffLoading, setVscodeDiffLoading] = useState(false); + const [vscodeDiffError, setVscodeDiffError] = useState(null); + + const canOpenVscodeDiff = !!(currentPlan && basePlan && baseVersion != null); + + const handleOpenVscodeDiff = async () => { + if (!canOpenVscodeDiff) return; + setVscodeDiffLoading(true); + setVscodeDiffError(null); + try { + const res = await fetch("/api/plan/vscode-diff", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ basePlan, currentPlan, baseVersion }), + }); + const data = await res.json() as { ok?: boolean; error?: string }; + if (!res.ok || data.error) { + setVscodeDiffError(data.error || "Failed to open VS Code diff"); + } + } catch { + setVscodeDiffError("Failed to connect to server"); + } finally { + setVscodeDiffLoading(false); + } + }; + return (
@@ -97,7 +129,7 @@ export const PlanDiffViewer: React.FC = ({
- {/* Diff mode switcher + version label */} + {/* Diff mode switcher + version label + VS Code button */}
{baseVersionLabel && ( @@ -105,8 +137,36 @@ export const PlanDiffViewer: React.FC = ({ vs {baseVersionLabel} )} + {canOpenVscodeDiff && ( + + )}
+ {/* VS Code diff error message */} + {vscodeDiffError && ( +
+ {vscodeDiffError} + +
+ )} + {/* Diff content */} {diffMode === "clean" ? ( From 3a9e7329414861d31cef26d450dafc4a5d65fa8b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 01:10:42 +0000 Subject: [PATCH 03/10] chore: update bun.lock after install https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW --- bun.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 3fff9f8..2a03f35 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "plannotator", @@ -52,7 +53,7 @@ }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.8.5", + "version": "0.9.2", "dependencies": { "@opencode-ai/plugin": "^1.1.10", }, @@ -65,7 +66,7 @@ }, "apps/pi-extension": { "name": "@plannotator/pi-extension", - "version": "0.8.5", + "version": "0.9.2", "peerDependencies": { "@mariozechner/pi-coding-agent": ">=0.53.0", }, @@ -132,7 +133,7 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.8.5", + "version": "0.9.2", "peerDependencies": { "bun": ">=1.0.0", }, From dc9ad796a9d6353efb04f0f47989c10bbbb8f363 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 01:12:30 +0000 Subject: [PATCH 04/10] refactor: server reads plan versions directly instead of receiving from client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server already has the current plan in its closure and can read any version from disk via getPlanVersion(). No need to send plan text from the UI — just send baseVersion. https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW --- packages/editor/App.tsx | 2 -- packages/server/index.ts | 15 +++++++++------ .../ui/components/plan-diff/PlanDiffViewer.tsx | 8 ++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index ad009fa..7c3431f 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -1170,8 +1170,6 @@ const App: React.FC = () => { onPlanDiffToggle={() => setIsPlanDiffActive(false)} repoInfo={repoInfo} baseVersionLabel={planDiff.diffBaseVersion != null ? `v${planDiff.diffBaseVersion}` : undefined} - currentPlan={markdown} - basePlan={planDiff.diffBasePlan ?? undefined} baseVersion={planDiff.diffBaseVersion ?? undefined} /> ) : ( diff --git a/packages/server/index.ts b/packages/server/index.ts index ee0baae..1725728 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -245,21 +245,24 @@ export async function startPlannotatorServer( if (url.pathname === "/api/plan/vscode-diff" && req.method === "POST") { try { const body = (await req.json()) as { - basePlan: string; - currentPlan: string; baseVersion: number; }; - if (!body.basePlan || !body.currentPlan) { - return Response.json({ error: "Missing basePlan or currentPlan" }, { status: 400 }); + if (!body.baseVersion) { + return Response.json({ error: "Missing baseVersion" }, { status: 400 }); + } + + const basePlan = getPlanVersion(project, slug, body.baseVersion); + if (!basePlan) { + return Response.json({ error: `Version ${body.baseVersion} not found` }, { status: 404 }); } mkdirSync(UPLOAD_DIR, { recursive: true }); const oldPath = `${UPLOAD_DIR}/plan-v${body.baseVersion}.md`; const newPath = `${UPLOAD_DIR}/plan-current.md`; - await Bun.write(oldPath, body.basePlan); - await Bun.write(newPath, body.currentPlan); + await Bun.write(oldPath, basePlan); + await Bun.write(newPath, plan); const proc = Bun.spawn(["code", "--diff", oldPath, newPath], { stdout: "ignore", diff --git a/packages/ui/components/plan-diff/PlanDiffViewer.tsx b/packages/ui/components/plan-diff/PlanDiffViewer.tsx index 2de414d..6cfd8fc 100644 --- a/packages/ui/components/plan-diff/PlanDiffViewer.tsx +++ b/packages/ui/components/plan-diff/PlanDiffViewer.tsx @@ -24,8 +24,6 @@ interface PlanDiffViewerProps { onPlanDiffToggle: () => void; repoInfo?: { display: string; branch?: string } | null; baseVersionLabel?: string; - currentPlan?: string; - basePlan?: string; baseVersion?: number; } @@ -37,14 +35,12 @@ export const PlanDiffViewer: React.FC = ({ onPlanDiffToggle, repoInfo, baseVersionLabel, - currentPlan, - basePlan, baseVersion, }) => { const [vscodeDiffLoading, setVscodeDiffLoading] = useState(false); const [vscodeDiffError, setVscodeDiffError] = useState(null); - const canOpenVscodeDiff = !!(currentPlan && basePlan && baseVersion != null); + const canOpenVscodeDiff = baseVersion != null; const handleOpenVscodeDiff = async () => { if (!canOpenVscodeDiff) return; @@ -54,7 +50,7 @@ export const PlanDiffViewer: React.FC = ({ const res = await fetch("/api/plan/vscode-diff", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ basePlan, currentPlan, baseVersion }), + body: JSON.stringify({ baseVersion }), }); const data = await res.json() as { ok?: boolean; error?: string }; if (!res.ok || data.error) { From a6f517ad3a88c9f1faed5ec04f6485bb6cf7f607 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Tue, 24 Feb 2026 17:31:29 -0800 Subject: [PATCH 05/10] chore: remove plan.md scoping document from repo Co-Authored-By: Claude Opus 4.6 --- plan.md | 87 --------------------------------------------------------- 1 file changed, 87 deletions(-) delete mode 100644 plan.md diff --git a/plan.md b/plan.md deleted file mode 100644 index 56fb373..0000000 --- a/plan.md +++ /dev/null @@ -1,87 +0,0 @@ -# Feature: "Open in VS Code Diff" Button - -## Overview - -Add a button to the PlanDiffViewer toolbar that opens the current plan diff in VS Code's native side-by-side diff viewer. When clicked, the server writes both versions (base and current) to temp files and invokes `code --diff `. - -## Context - -The PlanDiffViewer already shows diffs in two modes (rendered + raw). Adding a VS Code diff option gives power users access to VS Code's mature diff features: inline editing, word-level highlighting, minimap, search, etc. - -## Changes - -### 1. New server endpoint: `POST /api/plan/vscode-diff` - -**File:** `packages/server/index.ts` - -Add a new endpoint that: -- Accepts `{ basePlan: string, currentPlan: string, baseVersion: number }` in the request body -- Writes both plans to temp files in `/tmp/plannotator/`: - - `/tmp/plannotator/plan-v{N}.md` (base version) - - `/tmp/plannotator/plan-current.md` (current version) -- Spawns `code --diff ` via `Bun.spawn()` -- Returns `{ ok: true }` on success, or `{ error: string }` on failure -- If `code` CLI isn't found, returns a 400 with a helpful error message suggesting `Shell Command: Install 'code' command in PATH` - -The endpoint reuses the existing `UPLOAD_DIR` (`/tmp/plannotator/`) directory pattern already used for image uploads. - -### 2. UI button in PlanDiffViewer toolbar - -**File:** `packages/ui/components/plan-diff/PlanDiffViewer.tsx` - -Add a "VS Code" button in the diff mode switcher row (next to `PlanDiffModeSwitcher` and the version label). The button: -- Has the VS Code icon (simple inline SVG) and text "VS Code" (hidden on mobile, icon-only) -- Calls `POST /api/plan/vscode-diff` with the base plan + current plan -- Shows a brief loading state while the request is in flight -- On error, displays a toast/inline message - -**File:** `packages/ui/components/plan-diff/PlanDiffViewer.tsx` (props update) - -New props needed: -- `currentPlan: string` — the current plan markdown -- `basePlan: string` — the base version markdown -- `baseVersion: number` — the base version number (for temp file naming) - -These are already available in the parent (`App.tsx`) via the `usePlanDiff` hook (`planDiff.diffBasePlan`, `markdown`, `planDiff.diffBaseVersion`). - -### 3. Wire up in App.tsx - -**File:** `packages/editor/App.tsx` - -Pass the three new props to `PlanDiffViewer`: -```tsx - -``` - -### 4. Build and verify - -Run `bun run build:hook` to bundle the updated UI into the single-file HTML served by the hook server. - -## Design Decisions - -1. **Server-side `code --diff` invocation:** The UI runs in a browser but needs to execute a shell command. The Bun server already has full system access (it opens the browser, reads files, etc.), so adding a shell spawn is consistent with the architecture. - -2. **Temp files instead of piping:** VS Code `--diff` requires two file paths. Temp files in `/tmp/plannotator/` are the simplest approach and align with the existing image upload pattern. Files are overwritten on each click (no accumulation). - -3. **No new dependencies:** Uses `Bun.spawn()` for process execution and existing temp directory patterns. No new npm packages needed. - -4. **Graceful degradation:** If VS Code CLI isn't available, the error message tells the user how to install it. The button is always visible (not feature-detected) since the server environment may differ from what the browser can detect. - -## Files Modified - -| File | Change | -|------|--------| -| `packages/server/index.ts` | Add `POST /api/plan/vscode-diff` endpoint | -| `packages/ui/components/plan-diff/PlanDiffViewer.tsx` | Add VS Code button, new props | -| `packages/editor/App.tsx` | Pass new props to PlanDiffViewer | - -## Out of Scope - -- Opening in other editors (Cursor, Zed, etc.) — could be a follow-up with `PLANNOTATOR_EDITOR` env var -- Detecting whether VS Code is installed before showing the button -- Adding this to the code review diff viewer (`review-editor`) — separate feature From 3b4a7e894a3780c4dc2945b41f6ab4817bf41a09 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Tue, 24 Feb 2026 17:44:04 -0800 Subject: [PATCH 06/10] refactor: extract editor diff logic into shared packages/server/editor.ts Co-Authored-By: Claude Opus 4.6 --- packages/server/editor.ts | 54 +++++++++++++++++++++++++++++++++++++++ packages/server/index.ts | 40 +++++------------------------ 2 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 packages/server/editor.ts diff --git a/packages/server/editor.ts b/packages/server/editor.ts new file mode 100644 index 0000000..ba2c6dc --- /dev/null +++ b/packages/server/editor.ts @@ -0,0 +1,54 @@ +/** + * Editor diff utility — opens plan diffs in external editors + */ + +import { mkdirSync, writeFileSync } from "fs"; +import { UPLOAD_DIR } from "./image"; + +/** + * Write two plan versions to temp files and open them in VS Code's diff viewer. + * + * Returns `{ ok: true }` on success or `{ error: string }` on failure. + */ +export async function openEditorDiff( + oldContent: string, + newContent: string, + opts: { baseVersion: number } +): Promise<{ ok: true } | { error: string }> { + mkdirSync(UPLOAD_DIR, { recursive: true }); + const oldPath = `${UPLOAD_DIR}/plan-v${opts.baseVersion}.md`; + const newPath = `${UPLOAD_DIR}/plan-current.md`; + + writeFileSync(oldPath, oldContent); + writeFileSync(newPath, newContent); + + try { + const proc = Bun.spawn(["code", "--diff", oldPath, newPath], { + stdout: "ignore", + stderr: "pipe", + }); + const exitCode = await proc.exited; + + if (exitCode !== 0) { + const stderr = await new Response(proc.stderr).text(); + if (stderr.includes("not found") || stderr.includes("ENOENT")) { + return { + error: + "VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette.", + }; + } + return { error: `code --diff exited with ${exitCode}: ${stderr}` }; + } + + return { ok: true }; + } catch (err) { + const msg = err instanceof Error ? err.message : "Failed to open diff"; + if (msg.includes("ENOENT") || msg.includes("not found")) { + return { + error: + "VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette.", + }; + } + return { error: msg }; + } +} diff --git a/packages/server/index.ts b/packages/server/index.ts index 1725728..2af847d 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -13,6 +13,7 @@ import { mkdirSync } from "fs"; import { isRemoteSession, getServerPort } from "./remote"; import { openBrowser } from "./browser"; import { validateImagePath, validateUploadExtension, UPLOAD_DIR } from "./image"; +import { openEditorDiff } from "./editor"; import { detectObsidianVaults, saveToObsidian, @@ -244,9 +245,7 @@ export async function startPlannotatorServer( // API: Open plan diff in VS Code if (url.pathname === "/api/plan/vscode-diff" && req.method === "POST") { try { - const body = (await req.json()) as { - baseVersion: number; - }; + const body = (await req.json()) as { baseVersion: number }; if (!body.baseVersion) { return Response.json({ error: "Missing baseVersion" }, { status: 400 }); @@ -257,41 +256,14 @@ export async function startPlannotatorServer( return Response.json({ error: `Version ${body.baseVersion} not found` }, { status: 404 }); } - mkdirSync(UPLOAD_DIR, { recursive: true }); - const oldPath = `${UPLOAD_DIR}/plan-v${body.baseVersion}.md`; - const newPath = `${UPLOAD_DIR}/plan-current.md`; - - await Bun.write(oldPath, basePlan); - await Bun.write(newPath, plan); - - const proc = Bun.spawn(["code", "--diff", oldPath, newPath], { - stdout: "ignore", - stderr: "pipe", - }); - const exitCode = await proc.exited; - - if (exitCode !== 0) { - const stderr = await new Response(proc.stderr).text(); - const isNotFound = stderr.includes("not found") || stderr.includes("ENOENT"); - if (isNotFound) { - return Response.json( - { error: "VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette." }, - { status: 400 } - ); - } - return Response.json({ error: `code --diff exited with ${exitCode}: ${stderr}` }, { status: 500 }); + const result = await openEditorDiff(basePlan, plan, { baseVersion: body.baseVersion }); + if ("error" in result) { + const status = result.error.includes("not found") ? 400 : 500; + return Response.json({ error: result.error }, { status }); } - return Response.json({ ok: true }); } catch (err) { const message = err instanceof Error ? err.message : "Failed to open VS Code diff"; - const isNotFound = message.includes("ENOENT") || message.includes("not found"); - if (isNotFound) { - return Response.json( - { error: "VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette." }, - { status: 400 } - ); - } return Response.json({ error: message }, { status: 500 }); } } From fe81b8d2c27b45416a004dcb84bb7a35e590e62f Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 11:28:46 -0800 Subject: [PATCH 07/10] feat: use official VS Code SVG icon in plan diff viewer button Co-Authored-By: Claude Opus 4.6 --- .../components/plan-diff/PlanDiffViewer.tsx | 5 +- .../ui/components/plan-diff/VSCodeIcon.tsx | 132 ++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 packages/ui/components/plan-diff/VSCodeIcon.tsx diff --git a/packages/ui/components/plan-diff/PlanDiffViewer.tsx b/packages/ui/components/plan-diff/PlanDiffViewer.tsx index 6cfd8fc..4b320c1 100644 --- a/packages/ui/components/plan-diff/PlanDiffViewer.tsx +++ b/packages/ui/components/plan-diff/PlanDiffViewer.tsx @@ -15,6 +15,7 @@ import { import { PlanCleanDiffView } from "./PlanCleanDiffView"; import { PlanRawDiffView } from "./PlanRawDiffView"; import { PlanDiffBadge } from "./PlanDiffBadge"; +import { VSCodeIcon } from "./VSCodeIcon"; interface PlanDiffViewerProps { diffBlocks: PlanDiffBlock[]; @@ -140,9 +141,7 @@ export const PlanDiffViewer: React.FC = ({ className="ml-auto flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 border border-border/30 transition-colors disabled:opacity-50" title="Open diff in VS Code" > - - - + {vscodeDiffLoading ? "Opening..." : "VS Code"} diff --git a/packages/ui/components/plan-diff/VSCodeIcon.tsx b/packages/ui/components/plan-diff/VSCodeIcon.tsx new file mode 100644 index 0000000..1bf9a9c --- /dev/null +++ b/packages/ui/components/plan-diff/VSCodeIcon.tsx @@ -0,0 +1,132 @@ +import React from "react"; + +export const VSCodeIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); From 356e1f0ec36b67545f5ee205c3d96486ca5c91be Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 12:33:03 -0800 Subject: [PATCH 08/10] refactor: use history file paths directly for VS Code diff instead of writing temp files Both plan versions already exist on disk in ~/.plannotator/history/. Eliminates redundant file writes, fixes UPLOAD_DIR semantic misuse, and corrects HTTP status codes for editor errors. Co-Authored-By: Claude Opus 4.6 --- packages/server/editor.ts | 17 +++-------------- packages/server/index.ts | 11 ++++++----- packages/server/storage.ts | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/server/editor.ts b/packages/server/editor.ts index ba2c6dc..18058ba 100644 --- a/packages/server/editor.ts +++ b/packages/server/editor.ts @@ -2,26 +2,15 @@ * Editor diff utility — opens plan diffs in external editors */ -import { mkdirSync, writeFileSync } from "fs"; -import { UPLOAD_DIR } from "./image"; - /** - * Write two plan versions to temp files and open them in VS Code's diff viewer. + * Open two files in VS Code's diff viewer. * * Returns `{ ok: true }` on success or `{ error: string }` on failure. */ export async function openEditorDiff( - oldContent: string, - newContent: string, - opts: { baseVersion: number } + oldPath: string, + newPath: string ): Promise<{ ok: true } | { error: string }> { - mkdirSync(UPLOAD_DIR, { recursive: true }); - const oldPath = `${UPLOAD_DIR}/plan-v${opts.baseVersion}.md`; - const newPath = `${UPLOAD_DIR}/plan-current.md`; - - writeFileSync(oldPath, oldContent); - writeFileSync(newPath, newContent); - try { const proc = Bun.spawn(["code", "--diff", oldPath, newPath], { stdout: "ignore", diff --git a/packages/server/index.ts b/packages/server/index.ts index 2af847d..a839809 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -29,6 +29,7 @@ import { saveFinalSnapshot, saveToHistory, getPlanVersion, + getPlanVersionPath, getVersionCount, listVersions, listProjectPlans, @@ -117,6 +118,7 @@ export async function startPlannotatorServer( // Version history: save plan and detect previous version const project = (await detectProjectName()) ?? "_unknown"; const historyResult = saveToHistory(project, slug, plan); + const currentPlanPath = historyResult.path; const previousPlan = historyResult.version > 1 ? getPlanVersion(project, slug, historyResult.version - 1) @@ -251,15 +253,14 @@ export async function startPlannotatorServer( return Response.json({ error: "Missing baseVersion" }, { status: 400 }); } - const basePlan = getPlanVersion(project, slug, body.baseVersion); - if (!basePlan) { + const basePath = getPlanVersionPath(project, slug, body.baseVersion); + if (!basePath) { return Response.json({ error: `Version ${body.baseVersion} not found` }, { status: 404 }); } - const result = await openEditorDiff(basePlan, plan, { baseVersion: body.baseVersion }); + const result = await openEditorDiff(basePath, currentPlanPath); if ("error" in result) { - const status = result.error.includes("not found") ? 400 : 500; - return Response.json({ error: result.error }, { status }); + return Response.json({ error: result.error }, { status: 500 }); } return Response.json({ ok: true }); } catch (err) { diff --git a/packages/server/storage.ts b/packages/server/storage.ts index c841c4e..857e293 100644 --- a/packages/server/storage.ts +++ b/packages/server/storage.ts @@ -7,7 +7,7 @@ import { homedir } from "os"; import { join } from "path"; -import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from "fs"; +import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } from "fs"; import { sanitizeTag } from "./project"; /** @@ -186,6 +186,21 @@ export function getPlanVersion( } } +/** + * Get the file path for a specific version in history. + * Returns null if the version file doesn't exist. + */ +export function getPlanVersionPath( + project: string, + slug: string, + version: number +): string | null { + const historyDir = join(homedir(), ".plannotator", "history", project, slug); + const fileName = `${String(version).padStart(3, "0")}.md`; + const filePath = join(historyDir, fileName); + return existsSync(filePath) ? filePath : null; +} + /** * Get the number of versions stored for a project/slug. * Returns 0 if the directory doesn't exist. From 98cd0a90d3a254f61bba18e1f6c5e6e753c38922 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 12:35:01 -0800 Subject: [PATCH 09/10] refactor: rename editor.ts to ide.ts to avoid confusion with plan editor UI Co-Authored-By: Claude Opus 4.6 --- packages/server/{editor.ts => ide.ts} | 2 +- packages/server/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/server/{editor.ts => ide.ts} (95%) diff --git a/packages/server/editor.ts b/packages/server/ide.ts similarity index 95% rename from packages/server/editor.ts rename to packages/server/ide.ts index 18058ba..6ded965 100644 --- a/packages/server/editor.ts +++ b/packages/server/ide.ts @@ -1,5 +1,5 @@ /** - * Editor diff utility — opens plan diffs in external editors + * IDE integration — opens plan diffs in external IDEs */ /** diff --git a/packages/server/index.ts b/packages/server/index.ts index a839809..8d2c3b4 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -13,7 +13,7 @@ import { mkdirSync } from "fs"; import { isRemoteSession, getServerPort } from "./remote"; import { openBrowser } from "./browser"; import { validateImagePath, validateUploadExtension, UPLOAD_DIR } from "./image"; -import { openEditorDiff } from "./editor"; +import { openEditorDiff } from "./ide"; import { detectObsidianVaults, saveToObsidian, From 60729db178f220ffc19b7d63c3a34bc039acf172 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 13:24:27 -0800 Subject: [PATCH 10/10] docs: add ide.ts and vscode-diff endpoint to CLAUDE.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2d3fe48..b0e1f08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,7 @@ plannotator/ │ │ ├── remote.ts # isRemoteSession(), getServerPort() │ │ ├── browser.ts # openBrowser() │ │ ├── integrations.ts # Obsidian, Bear integrations +│ │ ├── ide.ts # VS Code diff integration (openEditorDiff) │ │ └── project.ts # Project name detection for tags │ ├── ui/ # Shared React components │ │ ├── components/ # Viewer, Toolbar, Settings, etc. @@ -149,6 +150,7 @@ Send Annotations → feedback sent to agent session | `/api/image` | GET | Serve image by path query param | | `/api/upload` | POST | Upload image, returns `{ path, originalName }` | | `/api/obsidian/vaults`| GET | Detect available Obsidian vaults | +| `/api/plan/vscode-diff` | POST | Open diff in VS Code (body: baseVersion) | ### Review Server (`packages/server/review.ts`)