From 5fde2e23d965059f89853f900b742755e23e3683 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 9 Mar 2026 07:05:18 -0700 Subject: [PATCH 1/4] feat: add per-file git add from code review UI (#254) Adds the ability to `git add` individual files directly from the code review interface. Users can stage approved files then switch to the new "Unstaged changes" diff view to see only remaining work. - POST /api/git-add endpoint with worktree support - gitAddFile/gitResetFile utilities in packages/server/git.ts - "Staged changes" and "Unstaged changes" added to diff type dropdown - useGitAdd hook encapsulating all staging state and API logic - "Git Add" / "Added" toggle button in FileHeader (next to Viewed) - Visual indicator (left border) for staged files in sidebar FileTree Co-Authored-By: Claude Opus 4.6 --- packages/review-editor/App.tsx | 15 +++- .../review-editor/components/DiffViewer.tsx | 12 ++++ .../review-editor/components/FileHeader.tsx | 40 ++++++++++- .../review-editor/components/FileTree.tsx | 11 +++ .../review-editor/components/FileTreeNode.tsx | 9 ++- packages/review-editor/hooks/useGitAdd.ts | 72 +++++++++++++++++++ packages/review-editor/index.css | 8 +++ packages/server/git.ts | 30 ++++++++ packages/server/review.ts | 30 +++++++- 9 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 packages/review-editor/hooks/useGitAdd.ts diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index e97ee5e6e..9352e9ed6 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -11,6 +11,7 @@ import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/u import { CodeAnnotation, CodeAnnotationType, SelectedLineRange } from '@plannotator/ui/types'; import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel'; import { useCodeAnnotationDraft } from '@plannotator/ui/hooks/useCodeAnnotationDraft'; +import { useGitAdd } from './hooks/useGitAdd'; import { useEditorAnnotations } from '@plannotator/ui/hooks/useEditorAnnotations'; import { exportEditorAnnotations } from '@plannotator/ui/utils/parser'; import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle'; @@ -361,6 +362,12 @@ const ReviewApp: React.FC = () => { return { activeWorktreePath: null, activeDiffBase: diffType }; }, [diffType]); + // Git add/staging logic + const { stagedFiles, stagingFile, canStageFiles, stageFile, resetStagedFiles, stageError } = useGitAdd({ + activeDiffBase, + onFileViewed: (path) => setViewedFiles(prev => new Set(prev).add(path)), + }); + // Shared helper: fetch a diff switch and update state const fetchDiffSwitch = useCallback(async (fullDiffType: string) => { setIsLoadingDiff(true); @@ -385,13 +392,14 @@ const ReviewApp: React.FC = () => { setActiveFileIndex(0); setPendingSelection(null); setDiffError(data.error || null); + resetStagedFiles(); } catch (err) { console.error('Failed to switch diff:', err); setDiffError(err instanceof Error ? err.message : 'Failed to switch diff'); } finally { setIsLoadingDiff(false); } - }, []); + }, [resetStagedFiles]); // Switch diff type (uncommitted, last-commit, branch) — composes worktree prefix if active const handleDiffSwitch = useCallback(async (baseDiffType: string) => { @@ -808,6 +816,7 @@ const ReviewApp: React.FC = () => { activeWorktreePath={activeWorktreePath} onSelectWorktree={handleWorktreeSwitch} currentBranch={gitContext?.currentBranch} + stagedFiles={stagedFiles} /> @@ -841,6 +850,10 @@ const ReviewApp: React.FC = () => { onDeleteAnnotation={handleDeleteAnnotation} isViewed={viewedFiles.has(activeFile.path)} onToggleViewed={() => handleToggleViewed(activeFile.path)} + isStaged={stagedFiles.has(activeFile.path)} + isStaging={stagingFile === activeFile.path} + onStage={() => stageFile(activeFile.path)} + canStage={canStageFiles} /> ) : (
diff --git a/packages/review-editor/components/DiffViewer.tsx b/packages/review-editor/components/DiffViewer.tsx index ac01c0c16..69089c179 100644 --- a/packages/review-editor/components/DiffViewer.tsx +++ b/packages/review-editor/components/DiffViewer.tsx @@ -25,6 +25,10 @@ interface DiffViewerProps { onDeleteAnnotation: (id: string) => void; isViewed?: boolean; onToggleViewed?: () => void; + isStaged?: boolean; + isStaging?: boolean; + onStage?: () => void; + canStage?: boolean; } export const DiffViewer: React.FC = ({ @@ -42,6 +46,10 @@ export const DiffViewer: React.FC = ({ onDeleteAnnotation, isViewed = false, onToggleViewed, + isStaged = false, + isStaging = false, + onStage, + canStage = false, }) => { const { theme } = useTheme(); const containerRef = useRef(null); @@ -180,6 +188,10 @@ export const DiffViewer: React.FC = ({ patch={patch} isViewed={isViewed} onToggleViewed={onToggleViewed} + isStaged={isStaged} + isStaging={isStaging} + onStage={onStage} + canStage={canStage} />
diff --git a/packages/review-editor/components/FileHeader.tsx b/packages/review-editor/components/FileHeader.tsx index 1491813bb..89b2fce58 100644 --- a/packages/review-editor/components/FileHeader.tsx +++ b/packages/review-editor/components/FileHeader.tsx @@ -5,14 +5,22 @@ interface FileHeaderProps { patch: string; isViewed?: boolean; onToggleViewed?: () => void; + isStaged?: boolean; + isStaging?: boolean; + onStage?: () => void; + canStage?: boolean; } -/** Sticky file header with file path, Viewed toggle, and Copy Diff button */ +/** Sticky file header with file path, Viewed toggle, Git Add, and Copy Diff button */ export const FileHeader: React.FC = ({ filePath, patch, isViewed = false, onToggleViewed, + isStaged = false, + isStaging = false, + onStage, + canStage = false, }) => { const [copied, setCopied] = useState(false); @@ -42,6 +50,36 @@ export const FileHeader: React.FC = ({ Viewed )} + {canStage && onStage && ( + + )} )} + {stageError && ( + {stageError} + )}