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`) 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", }, diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 4db80ac..7c3431f 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -1170,6 +1170,7 @@ const App: React.FC = () => { onPlanDiffToggle={() => setIsPlanDiffActive(false)} repoInfo={repoInfo} baseVersionLabel={planDiff.diffBaseVersion != null ? `v${planDiff.diffBaseVersion}` : undefined} + baseVersion={planDiff.diffBaseVersion ?? undefined} /> ) : ( { + 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 ea3b84d..8d2c3b4 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 "./ide"; import { detectObsidianVaults, saveToObsidian, @@ -28,6 +29,7 @@ import { saveFinalSnapshot, saveToHistory, getPlanVersion, + getPlanVersionPath, getVersionCount, listVersions, listProjectPlans, @@ -116,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) @@ -241,6 +244,31 @@ 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 }; + + if (!body.baseVersion) { + return Response.json({ error: "Missing baseVersion" }, { status: 400 }); + } + + const basePath = getPlanVersionPath(project, slug, body.baseVersion); + if (!basePath) { + return Response.json({ error: `Version ${body.baseVersion} not found` }, { status: 404 }); + } + + const result = await openEditorDiff(basePath, currentPlanPath); + if ("error" in result) { + return Response.json({ error: result.error }, { status: 500 }); + } + return Response.json({ ok: true }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to open VS Code diff"; + return Response.json({ error: message }, { status: 500 }); + } + } + // API: Detect Obsidian vaults if (url.pathname === "/api/obsidian/vaults") { const vaults = detectObsidianVaults(); 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. diff --git a/packages/ui/components/plan-diff/PlanDiffViewer.tsx b/packages/ui/components/plan-diff/PlanDiffViewer.tsx index f086e7c..4b320c1 100644 --- a/packages/ui/components/plan-diff/PlanDiffViewer.tsx +++ b/packages/ui/components/plan-diff/PlanDiffViewer.tsx @@ -6,7 +6,7 @@ * diff content instead of the annotatable plan. */ -import React from "react"; +import React, { useState } from "react"; import type { PlanDiffBlock, PlanDiffStats } from "../../utils/planDiffEngine"; import { PlanDiffModeSwitcher, @@ -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[]; @@ -24,6 +25,7 @@ interface PlanDiffViewerProps { onPlanDiffToggle: () => void; repoInfo?: { display: string; branch?: string } | null; baseVersionLabel?: string; + baseVersion?: number; } export const PlanDiffViewer: React.FC = ({ @@ -34,7 +36,34 @@ export const PlanDiffViewer: React.FC = ({ onPlanDiffToggle, repoInfo, baseVersionLabel, + baseVersion, }) => { + const [vscodeDiffLoading, setVscodeDiffLoading] = useState(false); + const [vscodeDiffError, setVscodeDiffError] = useState(null); + + const canOpenVscodeDiff = 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({ 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 +126,7 @@ export const PlanDiffViewer: React.FC = ({
- {/* Diff mode switcher + version label */} + {/* Diff mode switcher + version label + VS Code button */}
{baseVersionLabel && ( @@ -105,8 +134,34 @@ export const PlanDiffViewer: React.FC = ({ vs {baseVersionLabel} )} + {canOpenVscodeDiff && ( + + )}
+ {/* VS Code diff error message */} + {vscodeDiffError && ( +
+ {vscodeDiffError} + +
+ )} + {/* Diff content */} {diffMode === "clean" ? ( 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 }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +);