Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit e50786a

Browse files
authored
feat: filter viewed code review files (#3794)
1 parent 3e5f136 commit e50786a

7 files changed

Lines changed: 204 additions & 13 deletions

File tree

packages/ui/src/features/code-review/commentFileFilter.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,52 @@ export function filterReviewItemsByFilePaths(
6262
return filteredItems;
6363
}
6464

65+
export function filterReviewItemsByViewedState(
66+
items: ReviewListItem[],
67+
currentSignatures: ReadonlyMap<string, string>,
68+
viewedRecord: Readonly<Record<string, string>>,
69+
): ReviewListItem[] {
70+
const filteredItems: ReviewListItem[] = [];
71+
let pendingSectionItems: ReviewListItem[] = [];
72+
73+
for (const item of items) {
74+
if (!item.filePaths) {
75+
pendingSectionItems = [item];
76+
continue;
77+
}
78+
79+
const signature = item.scrollKey
80+
? currentSignatures.get(item.scrollKey)
81+
: undefined;
82+
if (
83+
signature !== undefined &&
84+
item.scrollKey !== undefined &&
85+
viewedRecord[item.scrollKey] === signature
86+
) {
87+
continue;
88+
}
89+
90+
filteredItems.push(...pendingSectionItems, item);
91+
pendingSectionItems = [];
92+
}
93+
94+
return filteredItems;
95+
}
96+
97+
export function resolveVisibleActiveFilePath(
98+
items: ReviewListItem[],
99+
activeFilePath: string | null,
100+
): string | null {
101+
if (
102+
activeFilePath &&
103+
items.some((item) => item.filePaths?.includes(activeFilePath))
104+
) {
105+
return activeFilePath;
106+
}
107+
108+
return items.find((item) => item.scrollKey)?.scrollKey ?? null;
109+
}
110+
65111
export function deriveCommentFileFilterState({
66112
items,
67113
requestedFilter,

packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ interface DiffSettingsMenuProps {
1616
unresolvedCommentedFileCount: number;
1717
commentFilter: CommentFileFilter;
1818
onCommentFilterChange?: (filter: CommentFileFilter) => void;
19+
hideViewedFiles: boolean;
20+
onHideViewedFilesChange: (hideViewed: boolean) => void;
1921
}
2022

2123
export function DiffSettingsMenu({
2224
commentedFileCount,
2325
unresolvedCommentedFileCount,
2426
commentFilter,
2527
onCommentFilterChange,
28+
hideViewedFiles,
29+
onHideViewedFilesChange,
2630
}: DiffSettingsMenuProps) {
2731
const wordWrap = useDiffViewerStore((s) => s.wordWrap);
2832
const toggleWordWrap = useDiffViewerStore((s) => s.toggleWordWrap);
@@ -79,6 +83,11 @@ export function DiffSettingsMenu({
7983
{hideWhitespaceChanges ? "Show whitespace" : "Hide whitespace"}
8084
</DropdownMenuItem>
8185
<DropdownMenuSeparator />
86+
<DropdownMenuItem
87+
onClick={() => onHideViewedFilesChange(!hideViewedFiles)}
88+
>
89+
{hideViewedFiles ? "Show viewed files" : "Hide viewed files"}
90+
</DropdownMenuItem>
8291
<DropdownMenuItem onClick={handleToggleReviewComments}>
8392
{showReviewComments ? "Hide review comments" : "Show review comments"}
8493
</DropdownMenuItem>

packages/ui/src/features/code-review/components/ReviewShell.tsx

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
import { VList, type VListHandle } from "virtua";
1717
import {
1818
deriveCommentFileFilterState,
19+
filterReviewItemsByViewedState,
1920
getEmptyReviewMessage,
2021
type ReviewListItem,
22+
resolveVisibleActiveFilePath,
2123
} from "../commentFileFilter";
2224
import {
2325
REVIEW_LIST_BUFFER_PX,
@@ -155,6 +157,12 @@ export function ReviewShell({
155157
const setCommentFileFilter = useReviewNavigationStore(
156158
(state) => state.setCommentFileFilter,
157159
);
160+
const hideViewedFiles = useReviewNavigationStore(
161+
(state) => state.hideViewedFiles[taskId] ?? false,
162+
);
163+
const setHideViewedFiles = useReviewNavigationStore(
164+
(state) => state.setHideViewedFiles,
165+
);
158166
const {
159167
activeFilter: activeCommentFilter,
160168
visibleItems,
@@ -170,9 +178,21 @@ export function ReviewShell({
170178
}),
171179
[commentFilter, commentedFilePaths, items, unresolvedCommentedFilePaths],
172180
);
181+
const filteredItems = useMemo(() => {
182+
if (!hideViewedFiles) return visibleItems;
183+
return filterReviewItemsByViewedState(
184+
visibleItems,
185+
currentSignatures,
186+
viewedRecord,
187+
);
188+
}, [currentSignatures, hideViewedFiles, viewedRecord, visibleItems]);
189+
const filteredFileCount = useMemo(
190+
() => filteredItems.filter((item) => item.filePaths).length,
191+
[filteredItems],
192+
);
173193
const visibleItemIndexByFilePath = useMemo(
174-
() => buildItemIndex(visibleItems),
175-
[visibleItems],
194+
() => buildItemIndex(filteredItems),
195+
[filteredItems],
176196
);
177197

178198
const workerFactory = useCallback(
@@ -253,8 +273,28 @@ export function ReviewShell({
253273
const setActiveFilePath = useReviewNavigationStore(
254274
(s) => s.setActiveFilePath,
255275
);
276+
const activeFilePath = useReviewNavigationStore(
277+
(s) => s.activeFilePaths[taskId] ?? null,
278+
);
256279
const clearTask = useReviewNavigationStore((s) => s.clearTask);
257280

281+
useEffect(() => {
282+
if (!hideViewedFiles || !activeFilePath) return;
283+
const nextActiveFilePath = resolveVisibleActiveFilePath(
284+
filteredItems,
285+
activeFilePath,
286+
);
287+
if (nextActiveFilePath === activeFilePath) return;
288+
lastActiveRef.current = nextActiveFilePath;
289+
setActiveFilePath(taskId, nextActiveFilePath);
290+
}, [
291+
activeFilePath,
292+
filteredItems,
293+
hideViewedFiles,
294+
setActiveFilePath,
295+
taskId,
296+
]);
297+
258298
useEffect(() => {
259299
return () => {
260300
if (navigationFrameRef.current !== null) {
@@ -348,11 +388,13 @@ export function ReviewShell({
348388
<Spinner size="2" />
349389
</Flex>
350390
);
351-
} else if (isEmpty || visibleItems.length === 0) {
391+
} else if (isEmpty || filteredItems.length === 0) {
352392
reviewContent = (
353393
<Flex align="center" justify="center" className="min-h-0 flex-1">
354394
<Text color="gray" className="text-sm">
355-
{getEmptyReviewMessage(activeCommentFilter)}
395+
{hideViewedFiles
396+
? "No unviewed file changes"
397+
: getEmptyReviewMessage(activeCommentFilter)}
356398
</Text>
357399
</Flex>
358400
);
@@ -366,7 +408,7 @@ export function ReviewShell({
366408
shift={false}
367409
style={{ scrollbarGutter: "stable" }}
368410
onScroll={handleScroll}
369-
data={visibleItems}
411+
data={filteredItems}
370412
>
371413
{renderItem}
372414
</VList>
@@ -413,6 +455,11 @@ export function ReviewShell({
413455
? (filter) => setCommentFileFilter(taskId, filter)
414456
: undefined
415457
}
458+
hideViewedFiles={hideViewedFiles}
459+
filteredFileCount={filteredFileCount}
460+
onHideViewedFilesChange={(hideViewed) =>
461+
setHideViewedFiles(taskId, hideViewed)
462+
}
416463
linesAdded={linesAdded}
417464
linesRemoved={linesRemoved}
418465
allExpanded={allExpanded}

packages/ui/src/features/code-review/components/ReviewToolbar.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ interface ReviewToolbarProps {
2828
unresolvedCommentedFileCount: number;
2929
commentFilter: CommentFileFilter;
3030
onCommentFilterChange?: (filter: CommentFileFilter) => void;
31+
hideViewedFiles: boolean;
32+
filteredFileCount: number;
33+
onHideViewedFilesChange: (hideViewed: boolean) => void;
3134
linesAdded: number;
3235
linesRemoved: number;
3336
allExpanded: boolean;
@@ -82,6 +85,9 @@ export const ReviewToolbar = memo(function ReviewToolbar({
8285
unresolvedCommentedFileCount,
8386
commentFilter,
8487
onCommentFilterChange,
88+
hideViewedFiles,
89+
filteredFileCount,
90+
onHideViewedFilesChange,
8591
allExpanded,
8692
onExpandAll,
8793
onCollapseAll,
@@ -108,13 +114,18 @@ export const ReviewToolbar = memo(function ReviewToolbar({
108114
setReviewMode(taskId, "closed");
109115
};
110116

111-
const { count: visibleFileCount, label: fileCountLabel } =
112-
getVisibleFileSummary(
113-
commentFilter,
114-
fileCount,
115-
commentedFileCount,
116-
unresolvedCommentedFileCount,
117-
);
117+
const visibleFileSummary = getVisibleFileSummary(
118+
commentFilter,
119+
fileCount,
120+
commentedFileCount,
121+
unresolvedCommentedFileCount,
122+
);
123+
const visibleFileCount = hideViewedFiles
124+
? filteredFileCount
125+
: visibleFileSummary.count;
126+
const fileCountLabel = hideViewedFiles
127+
? formatFileCount(filteredFileCount, "not viewed")
128+
: visibleFileSummary.label;
118129

119130
return (
120131
<Flex
@@ -129,7 +140,7 @@ export const ReviewToolbar = memo(function ReviewToolbar({
129140
>
130141
<Flex align="center" gap="2">
131142
<Text className="font-medium text-[13px]">{fileCountLabel}</Text>
132-
{visibleFileCount > 0 && (
143+
{!hideViewedFiles && visibleFileCount > 0 && (
133144
<Text className="text-(--gray-10) text-[13px]">
134145
{viewedCount}/{visibleFileCount} viewed
135146
</Text>
@@ -216,6 +227,8 @@ export const ReviewToolbar = memo(function ReviewToolbar({
216227
unresolvedCommentedFileCount={unresolvedCommentedFileCount}
217228
commentFilter={commentFilter}
218229
onCommentFilterChange={onCommentFilterChange}
230+
hideViewedFiles={hideViewedFiles}
231+
onHideViewedFilesChange={onHideViewedFilesChange}
219232
/>
220233

221234
<Tooltip content="Close review">

packages/ui/src/features/code-review/reviewNavigationStore.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe("reviewNavigationStore", () => {
99
reviewModes: {},
1010
selectedPrUrls: {},
1111
commentFileFilters: {},
12+
hideViewedFiles: {},
1213
});
1314
});
1415

@@ -28,6 +29,20 @@ describe("reviewNavigationStore", () => {
2829
).toBeUndefined();
2930
});
3031

32+
it("stores and clears the viewed-file filter per task", () => {
33+
const store = useReviewNavigationStore.getState();
34+
store.setHideViewedFiles("task-1", true);
35+
36+
expect(useReviewNavigationStore.getState().hideViewedFiles["task-1"]).toBe(
37+
true,
38+
);
39+
40+
store.clearTask("task-1");
41+
expect(useReviewNavigationStore.getState().hideViewedFiles["task-1"]).toBe(
42+
false,
43+
);
44+
});
45+
3146
it("clears the comment filter when navigating to a file", () => {
3247
const store = useReviewNavigationStore.getState();
3348
store.setCommentFileFilter("task-1", "unresolved");

packages/ui/src/features/code-review/reviewNavigationStore.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface ReviewNavigationStoreState {
99
reviewModes: Record<string, ReviewMode>;
1010
selectedPrUrls: Record<string, string | undefined>;
1111
commentFileFilters: Record<string, CommentFileFilter>;
12+
hideViewedFiles: Record<string, boolean>;
1213
}
1314

1415
interface ReviewNavigationStoreActions {
@@ -19,6 +20,7 @@ interface ReviewNavigationStoreActions {
1920
setReviewMode: (taskId: string, mode: ReviewMode) => void;
2021
setSelectedPrUrl: (taskId: string, url: string) => void;
2122
setCommentFileFilter: (taskId: string, filter: CommentFileFilter) => void;
23+
setHideViewedFiles: (taskId: string, hideViewed: boolean) => void;
2224
getReviewMode: (taskId: string) => ReviewMode;
2325
}
2426

@@ -32,6 +34,7 @@ export const useReviewNavigationStore = create<ReviewNavigationStore>()(
3234
reviewModes: {},
3335
selectedPrUrls: {},
3436
commentFileFilters: {},
37+
hideViewedFiles: {},
3538

3639
setActiveFilePath: (taskId, path) =>
3740
set((state) => ({
@@ -45,6 +48,7 @@ export const useReviewNavigationStore = create<ReviewNavigationStore>()(
4548
...state.commentFileFilters,
4649
[taskId]: "none",
4750
},
51+
hideViewedFiles: { ...state.hideViewedFiles, [taskId]: false },
4852
})),
4953

5054
clearScrollRequest: (taskId) =>
@@ -61,6 +65,7 @@ export const useReviewNavigationStore = create<ReviewNavigationStore>()(
6165
[taskId]: "none",
6266
},
6367
selectedPrUrls: { ...state.selectedPrUrls, [taskId]: undefined },
68+
hideViewedFiles: { ...state.hideViewedFiles, [taskId]: false },
6469
})),
6570

6671
setReviewMode: (taskId, mode) =>
@@ -87,6 +92,14 @@ export const useReviewNavigationStore = create<ReviewNavigationStore>()(
8792
},
8893
})),
8994

95+
setHideViewedFiles: (taskId, hideViewed) =>
96+
set((state) => ({
97+
hideViewedFiles: {
98+
...state.hideViewedFiles,
99+
[taskId]: hideViewed,
100+
},
101+
})),
102+
90103
getReviewMode: (taskId) => get().reviewModes[taskId] ?? "closed",
91104
}),
92105
);

0 commit comments

Comments
 (0)