diff --git a/bun.lock b/bun.lock
index a34b0beb..a5be0522 100644
--- a/bun.lock
+++ b/bun.lock
@@ -144,6 +144,7 @@
"version": "0.0.1",
"dependencies": {
"@pierre/diffs": "^1.1.0-beta.19",
+ "@plannotator/shared": "workspace:*",
"@plannotator/ui": "workspace:*",
"highlight.js": "^11.11.1",
"react": "^19.2.3",
diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx
index 9cf80f5f..e97ee5e6 100644
--- a/packages/review-editor/App.tsx
+++ b/packages/review-editor/App.tsx
@@ -18,6 +18,7 @@ import { DiffViewer } from './components/DiffViewer';
import { ReviewPanel } from './components/ReviewPanel';
import { FileTree } from './components/FileTree';
import { DEMO_DIFF } from './demoData';
+import type { DiffOption, WorktreeInfo, GitContext } from '@plannotator/shared/types';
declare const __APP_VERSION__: string;
@@ -29,17 +30,6 @@ interface DiffFile {
deletions: number;
}
-interface DiffOption {
- id: string;
- label: string;
-}
-
-interface GitContext {
- currentBranch: string;
- defaultBranch: string;
- diffOptions: DiffOption[];
-}
-
interface DiffData {
files: DiffFile[];
rawPatch: string;
@@ -355,16 +345,30 @@ const ReviewApp: React.FC = () => {
});
}, []);
- // Switch diff type (uncommitted, staged, last-commit, branch, etc.)
- const handleDiffSwitch = useCallback(async (newDiffType: string) => {
- if (newDiffType === diffType || newDiffType === 'separator') return;
+ // Derive worktree path and base diff type from the composite diffType string
+ const { activeWorktreePath, activeDiffBase } = useMemo(() => {
+ if (diffType.startsWith('worktree:')) {
+ const rest = diffType.slice('worktree:'.length);
+ const lastColon = rest.lastIndexOf(':');
+ if (lastColon !== -1) {
+ const sub = rest.slice(lastColon + 1);
+ if (['uncommitted', 'last-commit', 'branch'].includes(sub)) {
+ return { activeWorktreePath: rest.slice(0, lastColon), activeDiffBase: sub };
+ }
+ }
+ return { activeWorktreePath: rest, activeDiffBase: 'uncommitted' };
+ }
+ return { activeWorktreePath: null, activeDiffBase: diffType };
+ }, [diffType]);
+ // Shared helper: fetch a diff switch and update state
+ const fetchDiffSwitch = useCallback(async (fullDiffType: string) => {
setIsLoadingDiff(true);
try {
const res = await fetch('/api/diff/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ diffType: newDiffType }),
+ body: JSON.stringify({ diffType: fullDiffType }),
});
if (!res.ok) throw new Error('Failed to switch diff');
@@ -373,31 +377,39 @@ const ReviewApp: React.FC = () => {
rawPatch: string;
gitRef: string;
diffType: string;
- diffOptions?: { id: string; label: string }[];
error?: string;
};
- const newFiles = parseDiffToFiles(data.rawPatch);
- setFiles(newFiles);
+ setFiles(parseDiffToFiles(data.rawPatch));
setDiffType(data.diffType);
setActiveFileIndex(0);
setPendingSelection(null);
setDiffError(data.error || null);
-
- // Update dropdown options on worktree mode transitions
- if (data.diffOptions) {
- setGitContext(prev => prev ? { ...prev, diffOptions: data.diffOptions! } : prev);
- }
-
- // Note: We keep existing annotations - they may still be relevant
- // or user can clear them manually
} catch (err) {
console.error('Failed to switch diff:', err);
setDiffError(err instanceof Error ? err.message : 'Failed to switch diff');
} finally {
setIsLoadingDiff(false);
}
- }, [diffType]);
+ }, []);
+
+ // Switch diff type (uncommitted, last-commit, branch) — composes worktree prefix if active
+ const handleDiffSwitch = useCallback(async (baseDiffType: string) => {
+ const fullDiffType = activeWorktreePath
+ ? `worktree:${activeWorktreePath}:${baseDiffType}`
+ : baseDiffType;
+ if (fullDiffType === diffType) return;
+ await fetchDiffSwitch(fullDiffType);
+ }, [diffType, activeWorktreePath, fetchDiffSwitch]);
+
+ // Switch worktree context (or back to main repo)
+ const handleWorktreeSwitch = useCallback(async (worktreePath: string | null) => {
+ if (worktreePath === activeWorktreePath) return;
+ const fullDiffType = worktreePath
+ ? `worktree:${worktreePath}:uncommitted`
+ : 'uncommitted';
+ await fetchDiffSwitch(fullDiffType);
+ }, [activeWorktreePath, fetchDiffSwitch]);
// Select annotation - switches file if needed and scrolls to it
const handleSelectAnnotation = useCallback((id: string | null) => {
@@ -788,10 +800,14 @@ const ReviewApp: React.FC = () => {
onToggleHideViewed={() => setHideViewedFiles(prev => !prev)}
enableKeyboardNav={!showExportModal}
diffOptions={gitContext?.diffOptions}
- activeDiffType={diffType}
+ activeDiffType={activeDiffBase}
onSelectDiff={handleDiffSwitch}
isLoadingDiff={isLoadingDiff}
width={fileTreeResize.width}
+ worktrees={gitContext?.worktrees}
+ activeWorktreePath={activeWorktreePath}
+ onSelectWorktree={handleWorktreeSwitch}
+ currentBranch={gitContext?.currentBranch}
/>
- {diffType === 'uncommitted' && "No uncommitted changes to review."} - {diffType === 'staged' && "No staged changes. Stage some files with git add."} - {diffType === 'unstaged' && "No unstaged changes. All changes are staged."} - {diffType === 'last-commit' && "No changes in the last commit."} - {diffType === 'branch' && `No changes between this branch and ${gitContext?.defaultBranch || 'main'}.`} - {diffType?.startsWith('worktree:') && ( - diffType.endsWith(':last-commit') ? "No changes in the last commit in this worktree." : - diffType.endsWith(':branch') ? `No changes vs ${gitContext?.defaultBranch || 'main'} in this worktree.` : - "No uncommitted changes in this worktree." - )} + {activeDiffBase === 'uncommitted' && `No uncommitted changes${activeWorktreePath ? ' in this worktree' : ' to review'}.`} + {activeDiffBase === 'staged' && "No staged changes. Stage some files with git add."} + {activeDiffBase === 'unstaged' && "No unstaged changes. All changes are staged."} + {activeDiffBase === 'last-commit' && `No changes in the last commit${activeWorktreePath ? ' in this worktree' : ''}.`} + {activeDiffBase === 'branch' && `No changes vs ${gitContext?.defaultBranch || 'main'}${activeWorktreePath ? ' in this worktree' : ''}.`}
> )} diff --git a/packages/review-editor/components/FileTree.tsx b/packages/review-editor/components/FileTree.tsx index 4c283118..bd0e540d 100644 --- a/packages/review-editor/components/FileTree.tsx +++ b/packages/review-editor/components/FileTree.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useCallback, useState, useMemo } from 'react'; import { CodeAnnotation } from '@plannotator/ui/types'; +import type { DiffOption, WorktreeInfo } from '@plannotator/shared/types'; import { buildFileTree, getAncestorPaths, getAllFolderPaths } from '../utils/buildFileTree'; import { FileTreeNodeItem } from './FileTreeNode'; @@ -11,11 +12,6 @@ interface DiffFile { deletions: number; } -interface DiffOption { - id: string; - label: string; -} - interface FileTreeProps { files: DiffFile[]; activeFileIndex: number; @@ -31,6 +27,10 @@ interface FileTreeProps { onSelectDiff?: (diffType: string) => void; isLoadingDiff?: boolean; width?: number; + worktrees?: WorktreeInfo[]; + activeWorktreePath?: string | null; + onSelectWorktree?: (path: string | null) => void; + currentBranch?: string; } export const FileTree: React.FC