diff --git a/CLAUDE.md b/CLAUDE.md index 469d0a34d..fa9bc2f81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,6 +177,7 @@ Send Annotations → feedback sent to agent session | --------------------- | ------ | ------------------------------------------ | | `/api/diff` | GET | Returns `{ rawPatch, gitRef, origin }` | | `/api/file-content` | GET | Returns `{ oldContent, newContent }` for expandable diff context | +| `/api/git-add` | POST | Stage/unstage a file (body: `{ filePath, undo? }`) | | `/api/feedback` | POST | Submit review (body: feedback, annotations, agentSwitch) | | `/api/image` | GET | Serve image by path query param | | `/api/upload` | POST | Upload image, returns `{ path, originalName }` | diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index e97ee5e6e..c04200cf6 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'; @@ -352,7 +353,7 @@ const ReviewApp: React.FC = () => { const lastColon = rest.lastIndexOf(':'); if (lastColon !== -1) { const sub = rest.slice(lastColon + 1); - if (['uncommitted', 'last-commit', 'branch'].includes(sub)) { + if (['uncommitted', 'staged', 'unstaged', 'last-commit', 'branch'].includes(sub)) { return { activeWorktreePath: rest.slice(0, lastColon), activeDiffBase: sub }; } } @@ -361,6 +362,16 @@ const ReviewApp: React.FC = () => { return { activeWorktreePath: null, activeDiffBase: diffType }; }, [diffType]); + // Git add/staging logic + const handleFileViewedFromStage = useCallback( + (path: string) => setViewedFiles(prev => new Set(prev).add(path)), + [], + ); + const { stagedFiles, stagingFile, canStageFiles, stageFile, resetStagedFiles, stageError } = useGitAdd({ + activeDiffBase, + onFileViewed: handleFileViewedFromStage, + }); + // Shared helper: fetch a diff switch and update state const fetchDiffSwitch = useCallback(async (fullDiffType: string) => { setIsLoadingDiff(true); @@ -385,13 +396,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 +820,7 @@ const ReviewApp: React.FC = () => { activeWorktreePath={activeWorktreePath} onSelectWorktree={handleWorktreeSwitch} currentBranch={gitContext?.currentBranch} + stagedFiles={stagedFiles} /> @@ -841,6 +854,11 @@ 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} + stageError={stageError} /> ) : (
diff --git a/packages/review-editor/components/DiffViewer.tsx b/packages/review-editor/components/DiffViewer.tsx index ac01c0c16..2a3a9e216 100644 --- a/packages/review-editor/components/DiffViewer.tsx +++ b/packages/review-editor/components/DiffViewer.tsx @@ -25,6 +25,11 @@ interface DiffViewerProps { onDeleteAnnotation: (id: string) => void; isViewed?: boolean; onToggleViewed?: () => void; + isStaged?: boolean; + isStaging?: boolean; + onStage?: () => void; + canStage?: boolean; + stageError?: string | null; } export const DiffViewer: React.FC = ({ @@ -42,6 +47,11 @@ export const DiffViewer: React.FC = ({ onDeleteAnnotation, isViewed = false, onToggleViewed, + isStaged = false, + isStaging = false, + onStage, + canStage = false, + stageError, }) => { const { theme } = useTheme(); const containerRef = useRef(null); @@ -180,6 +190,11 @@ export const DiffViewer: React.FC = ({ patch={patch} isViewed={isViewed} onToggleViewed={onToggleViewed} + isStaged={isStaged} + isStaging={isStaging} + onStage={onStage} + canStage={canStage} + stageError={stageError} />
diff --git a/packages/review-editor/components/FileHeader.tsx b/packages/review-editor/components/FileHeader.tsx index 1491813bb..6a4d258a9 100644 --- a/packages/review-editor/components/FileHeader.tsx +++ b/packages/review-editor/components/FileHeader.tsx @@ -5,14 +5,24 @@ interface FileHeaderProps { patch: string; isViewed?: boolean; onToggleViewed?: () => void; + isStaged?: boolean; + isStaging?: boolean; + onStage?: () => void; + canStage?: boolean; + stageError?: string | null; } -/** 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, + stageError, }) => { const [copied, setCopied] = useState(false); @@ -42,6 +52,39 @@ export const FileHeader: React.FC = ({ Viewed )} + {canStage && onStage && ( + + )} + {stageError && ( + {stageError} + )}