From f654c5f3569af80889bcd36704783aac7fb6ce14 Mon Sep 17 00:00:00 2001 From: iliya Date: Wed, 4 Mar 2026 12:35:03 +0200 Subject: [PATCH] feat: add file-level accept/reject and diff tree filters Add Accept/Reject controls to each file header and add search + filters (unresolved/rejected/new) to the diff file tree. Made-with: Cursor --- .../team/review/ChangeReviewDialog.tsx | 78 +++++---- .../team/review/ContinuousScrollView.tsx | 12 +- .../team/review/FileSectionHeader.tsx | 18 +- .../components/team/review/ReviewFileTree.tsx | 164 ++++++++++++++++-- 4 files changed, 206 insertions(+), 66 deletions(-) diff --git a/src/renderer/components/team/review/ChangeReviewDialog.tsx b/src/renderer/components/team/review/ChangeReviewDialog.tsx index 97adf069..4ffcb471 100644 --- a/src/renderer/components/team/review/ChangeReviewDialog.tsx +++ b/src/renderer/components/team/review/ChangeReviewDialog.tsx @@ -122,6 +122,7 @@ export const ChangeReviewDialog = ({ const lastHunkActionAtRef = useRef>({}); const hunkDecisionUndoStackRef = useRef>({}); const newFileApplyInFlightRef = useRef(new Set()); + const lastFileActionAtRef = useRef(0); const removedNewFileUndoStackRef = useRef< { file: FileChangeSummary; index: number; restoreContent: string; removedAt: number }[] >([]); @@ -237,9 +238,10 @@ export const ChangeReviewDialog = ({ memberName, ]); - // Per-new-file accept/reject (Cursor-style) - const handleAcceptNewFile = useCallback( + // File-level accept/reject (Cursor-style) + const handleAcceptFile = useCallback( (filePath: string) => { + lastFileActionAtRef.current = Date.now(); acceptAllFile(filePath); const view = editorViewMapRef.current.get(filePath); if (view) { @@ -249,46 +251,52 @@ export const ChangeReviewDialog = ({ [acceptAllFile] ); - const handleRejectNewFile = useCallback( + const handleRejectFile = useCallback( async (filePath: string) => { if (newFileApplyInFlightRef.current.has(filePath)) return; newFileApplyInFlightRef.current.add(filePath); try { + const file = activeChangeSet?.files.find((f) => f.filePath === filePath); + const isNew = file?.isNewFile ?? false; + // Mark rejected in store + update CM view immediately for feedback + lastFileActionAtRef.current = Date.now(); rejectAllFile(filePath); const view = editorViewMapRef.current.get(filePath); if (view) { requestAnimationFrame(() => rejectAllChunks(view)); } - // Always apply immediately: rejecting a NEW file means deleting it from disk. - const file = activeChangeSet?.files.find((f) => f.filePath === filePath); - const isNew = file?.isNewFile ?? false; - if (!isNew) return; + if (REVIEW_INSTANT_APPLY) { + // Reject a whole file should apply immediately (restore original on disk), + // and NEW-file reject should delete it. + const result = await applySingleFileDecision(teamName, filePath, taskId, memberName); - const result = await applySingleFileDecision(teamName, filePath, taskId, memberName); - const hasErrorForFile = !!result?.errors.some((e) => e.filePath === filePath); - if (result && !hasErrorForFile && file) { - // Keep undo payload so Ctrl/Cmd+Z can restore the file (and re-add it to the review list). - const cachedModified = fileContents[filePath]?.modifiedFullContent; - const restoreContent = - cachedModified ?? - (() => { - const writeSnippets = file.snippets.filter( - (s) => !s.isError && (s.type === 'write-new' || s.type === 'write-update') - ); - if (writeSnippets.length === 0) return ''; - return writeSnippets[writeSnippets.length - 1].newString; - })(); - const index = activeChangeSet?.files.findIndex((f) => f.filePath === filePath) ?? 0; - removedNewFileUndoStackRef.current.push({ - file, - index: Math.max(0, index), - restoreContent, - removedAt: Date.now(), - }); - lastNewFileRemoveAtRef.current = Date.now(); - removeReviewFile(filePath); + if (isNew) { + const hasErrorForFile = !!result?.errors.some((e) => e.filePath === filePath); + if (result && !hasErrorForFile && file) { + // Keep undo payload so Ctrl/Cmd+Z can restore the file (and re-add it to the review list). + const cachedModified = fileContents[filePath]?.modifiedFullContent; + const restoreContent = + cachedModified ?? + (() => { + const writeSnippets = file.snippets.filter( + (s) => !s.isError && (s.type === 'write-new' || s.type === 'write-update') + ); + if (writeSnippets.length === 0) return ''; + return writeSnippets[writeSnippets.length - 1].newString; + })(); + const index = activeChangeSet?.files.findIndex((f) => f.filePath === filePath) ?? 0; + removedNewFileUndoStackRef.current.push({ + file, + index: Math.max(0, index), + restoreContent, + removedAt: Date.now(), + }); + lastNewFileRemoveAtRef.current = Date.now(); + removeReviewFile(filePath); + } + } } } finally { newFileApplyInFlightRef.current.delete(filePath); @@ -622,7 +630,11 @@ export const ChangeReviewDialog = ({ (max, v) => Math.max(max, v), 0 ); - const lastReviewActionAt = Math.max(lastBulkActionAtRef.current, lastHunkAt); + const lastReviewActionAt = Math.max( + lastBulkActionAtRef.current, + lastHunkAt, + lastFileActionAtRef.current + ); const newFileWasLastAction = lastNewFileRemoveAtRef.current >= lastReviewActionAt; const isInEditor = !!document.activeElement?.closest('.cm-editor'); const lastViewConnected = !!lastFocusedEditorRef.current?.dom.isConnected; @@ -949,8 +961,8 @@ export const ChangeReviewDialog = ({ onContentChanged={handleContentChanged} onDiscard={handleDiscardFile} onSave={handleSaveFile} - onAcceptNewFile={handleAcceptNewFile} - onRejectNewFile={handleRejectNewFile} + onAcceptFile={handleAcceptFile} + onRejectFile={handleRejectFile} onRestoreMissingFile={handleRestoreMissingFile} onVisibleFileChange={handleVisibleFileChange} scrollContainerRef={scrollContainerRef} diff --git a/src/renderer/components/team/review/ContinuousScrollView.tsx b/src/renderer/components/team/review/ContinuousScrollView.tsx index 968640d0..58fe705e 100644 --- a/src/renderer/components/team/review/ContinuousScrollView.tsx +++ b/src/renderer/components/team/review/ContinuousScrollView.tsx @@ -38,8 +38,8 @@ interface ContinuousScrollViewProps { onContentChanged: (filePath: string, content: string) => void; onDiscard: (filePath: string) => void; onSave: (filePath: string) => void; - onAcceptNewFile: (filePath: string) => void; - onRejectNewFile: (filePath: string) => void; + onAcceptFile: (filePath: string) => void; + onRejectFile: (filePath: string) => void; onRestoreMissingFile?: (filePath: string, content: string) => void; onVisibleFileChange: (filePath: string) => void; scrollContainerRef: React.RefObject; @@ -74,8 +74,8 @@ export const ContinuousScrollView = ({ onContentChanged, onDiscard, onSave, - onAcceptNewFile, - onRejectNewFile, + onAcceptFile, + onRejectFile, onRestoreMissingFile, onVisibleFileChange, scrollContainerRef, @@ -228,8 +228,8 @@ export const ContinuousScrollView = ({ onToggleCollapse={handleToggleCollapse} onDiscard={onDiscard} onSave={onSave} - onAcceptNewFile={onAcceptNewFile} - onRejectNewFile={onRejectNewFile} + onAcceptFile={onAcceptFile} + onRejectFile={onRejectFile} onRestoreMissingFile={onRestoreMissingFile} /> diff --git a/src/renderer/components/team/review/FileSectionHeader.tsx b/src/renderer/components/team/review/FileSectionHeader.tsx index cb02ef42..64278f2f 100644 --- a/src/renderer/components/team/review/FileSectionHeader.tsx +++ b/src/renderer/components/team/review/FileSectionHeader.tsx @@ -26,8 +26,8 @@ interface FileSectionHeaderProps { onDiscard: (filePath: string) => void; onSave: (filePath: string) => void; onRestoreMissingFile?: (filePath: string, content: string) => void; - onAcceptNewFile?: (filePath: string) => void; - onRejectNewFile?: (filePath: string) => void; + onAcceptFile?: (filePath: string) => void; + onRejectFile?: (filePath: string) => void; } export const FileSectionHeader = ({ @@ -41,8 +41,8 @@ export const FileSectionHeader = ({ onDiscard, onSave, onRestoreMissingFile, - onAcceptNewFile, - onRejectNewFile, + onAcceptFile, + onRejectFile, }: FileSectionHeaderProps): React.ReactElement => { const isMissingOnDisk = fileContent?.contentSource === 'unavailable'; const restoreContent = @@ -145,11 +145,11 @@ export const FileSectionHeader = ({ )}
- {file.isNewFile && (onAcceptNewFile || onRejectNewFile) && ( + {(onAcceptFile || onRejectFile) && (
- {onAcceptNewFile && ( + {onAcceptFile && ( )} - {onRejectNewFile && ( + {onRejectFile && ( + + + {(filterUnresolved || filterRejected || filterNew || normalizedQuery.length > 0) && ( + + )} +
+
+ + {filteredFiles.length === 0 ? ( +
No matching files
+ ) : ( +
+ {sortTreeNodes(tree).map((node) => ( + + ))} +
+ )} ); };