Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`)

Expand Down
7 changes: 4 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
) : (
<Viewer
Expand Down
43 changes: 43 additions & 0 deletions packages/server/ide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* IDE integration — opens plan diffs in external IDEs
*/

/**
* Open two files in VS Code's diff viewer.
*
* Returns `{ ok: true }` on success or `{ error: string }` on failure.
*/
export async function openEditorDiff(
oldPath: string,
newPath: string
): Promise<{ ok: true } | { error: string }> {
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 };
}
}
28 changes: 28 additions & 0 deletions packages/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,6 +29,7 @@ import {
saveFinalSnapshot,
saveToHistory,
getPlanVersion,
getPlanVersionPath,
getVersionCount,
listVersions,
listProjectPlans,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 16 additions & 1 deletion packages/server/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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.
Expand Down
59 changes: 57 additions & 2 deletions packages/ui/components/plan-diff/PlanDiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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[];
Expand All @@ -24,6 +25,7 @@ interface PlanDiffViewerProps {
onPlanDiffToggle: () => void;
repoInfo?: { display: string; branch?: string } | null;
baseVersionLabel?: string;
baseVersion?: number;
}

export const PlanDiffViewer: React.FC<PlanDiffViewerProps> = ({
Expand All @@ -34,7 +36,34 @@ export const PlanDiffViewer: React.FC<PlanDiffViewerProps> = ({
onPlanDiffToggle,
repoInfo,
baseVersionLabel,
baseVersion,
}) => {
const [vscodeDiffLoading, setVscodeDiffLoading] = useState(false);
const [vscodeDiffError, setVscodeDiffError] = useState<string | null>(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 (
<div className="relative z-50 w-full max-w-[832px] 2xl:max-w-5xl">
<article className="w-full max-w-[832px] 2xl:max-w-5xl bg-card border border-border/50 rounded-xl shadow-xl p-5 md:p-8 lg:p-10 xl:p-12 relative">
Expand Down Expand Up @@ -97,16 +126,42 @@ export const PlanDiffViewer: React.FC<PlanDiffViewerProps> = ({
</button>
</div>

{/* Diff mode switcher + version label */}
{/* Diff mode switcher + version label + VS Code button */}
<div className="mt-6 mb-6 flex items-center gap-3">
<PlanDiffModeSwitcher mode={diffMode} onChange={onDiffModeChange} />
{baseVersionLabel && (
<span className="text-[10px] text-muted-foreground">
vs {baseVersionLabel}
</span>
)}
{canOpenVscodeDiff && (
<button
onClick={handleOpenVscodeDiff}
disabled={vscodeDiffLoading}
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"
>
<VSCodeIcon className="w-3.5 h-3.5 flex-shrink-0" />
<span className="hidden md:inline">
{vscodeDiffLoading ? "Opening..." : "VS Code"}
</span>
</button>
)}
</div>

{/* VS Code diff error message */}
{vscodeDiffError && (
<div className="mb-4 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-xs text-destructive">
{vscodeDiffError}
<button
onClick={() => setVscodeDiffError(null)}
className="ml-2 text-destructive/60 hover:text-destructive"
>
dismiss
</button>
</div>
)}

{/* Diff content */}
{diffMode === "clean" ? (
<PlanCleanDiffView blocks={diffBlocks} />
Expand Down
Loading