From 729377cdcc62eb1e16cbb7be6275c012eebda807 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Mon, 20 Jul 2026 11:23:14 -0400 Subject: [PATCH 01/10] feat(code-review): filter files with comments Generated-By: PostHog Code Task-Id: 199c1147-fca6-415b-9ea1-00a45d9b7267 --- .../components/CloudReviewPage.tsx | 11 +- .../code-review/components/ReviewPage.tsx | 15 ++- .../code-review/components/ReviewShell.tsx | 127 +++++++++++++----- .../code-review/components/ReviewToolbar.tsx | 51 ++++++- .../components/reviewItemBuilders.tsx | 7 + .../code-review/reviewShellParts.test.tsx | 84 ++++++++++++ .../features/code-review/reviewShellParts.tsx | 35 +++++ 7 files changed, 287 insertions(+), 43 deletions(-) diff --git a/packages/ui/src/features/code-review/components/CloudReviewPage.tsx b/packages/ui/src/features/code-review/components/CloudReviewPage.tsx index fc45568bd8..8c3f03197a 100644 --- a/packages/ui/src/features/code-review/components/CloudReviewPage.tsx +++ b/packages/ui/src/features/code-review/components/CloudReviewPage.tsx @@ -11,6 +11,7 @@ import { useReviewNavigationStore } from "../reviewNavigationStore"; import { PatchedFileDiff } from "./PatchedFileDiff"; import { buildItemIndex, + getCommentedFilePaths, type ReviewListItem, ReviewShell, useReviewState, @@ -37,8 +38,12 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) { isLoading, } = useCloudChangedFiles(taskId, task, isReviewOpen); const { commentThreads } = usePrDetails(prUrl, { - includeComments: isReviewOpen && showReviewComments, + includeComments: isReviewOpen, }); + const commentedFilePaths = useMemo( + () => (prUrl ? getCommentedFilePaths(commentThreads) : undefined), + [commentThreads, prUrl], + ); const allPaths = useMemo(() => reviewFiles.map((f) => f.path), [reviewFiles]); @@ -83,6 +88,9 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) { return { key: file.path, scrollKey: file.path, + filePaths: [file.path, file.originalPath].filter( + (path): path is string => !!path, + ), node: ( s.showReviewComments); const { commentThreads } = usePrDetails(prUrl, { - includeComments: isReviewOpen && showReviewComments, + includeComments: isReviewOpen, }); const effectiveCommentThreads = showReviewComments ? commentThreads : undefined; + const commentedFilePaths = useMemo( + () => (prUrl ? getCommentedFilePaths(commentThreads) : undefined), + [commentThreads, prUrl], + ); const isLocalActive = isReviewOpen && effectiveSource === "local"; @@ -173,6 +178,7 @@ export function ReviewPage({ task }: ReviewPageProps) { branchSourceAvailable={branchSourceAvailable} prSourceAvailable={prSourceAvailable} commentThreads={effectiveCommentThreads} + commentedFilePaths={commentedFilePaths} /> ); } @@ -206,6 +212,7 @@ export function ReviewPage({ task }: ReviewPageProps) { untrackedFiles={untrackedFiles} stagedPathSet={stagedPathSet} commentThreads={effectiveCommentThreads} + commentedFilePaths={commentedFilePaths} effectiveSource={effectiveSource} branchSourceAvailable={branchSourceAvailable} prSourceAvailable={prSourceAvailable} @@ -242,6 +249,7 @@ function LocalReviewContent({ untrackedFiles, stagedPathSet, commentThreads, + commentedFilePaths, effectiveSource, branchSourceAvailable, prSourceAvailable, @@ -274,6 +282,7 @@ function LocalReviewContent({ untrackedFiles: ChangedFile[]; stagedPathSet: Set; commentThreads?: Map; + commentedFilePaths?: ReadonlySet; effectiveSource: ResolvedDiffSource; branchSourceAvailable: boolean; prSourceAvailable: boolean; @@ -437,6 +446,7 @@ function LocalReviewContent({ defaultBranch={defaultBranch} items={items} itemIndexByFilePath={itemIndexByFilePath} + commentedFilePaths={commentedFilePaths} currentSignatures={currentSignatures} viewedRecord={viewedRecord} onToggleViewed={toggleViewed} @@ -455,6 +465,7 @@ function RemoteReviewPage({ branchSourceAvailable, prSourceAvailable, commentThreads, + commentedFilePaths, }: { task: Task; repoPath: string | null; @@ -466,6 +477,7 @@ function RemoteReviewPage({ branchSourceAvailable: boolean; prSourceAvailable: boolean; commentThreads?: Map; + commentedFilePaths?: ReadonlySet; }) { const taskId = task.id; const isBranch = effectiveSource === "branch"; @@ -548,6 +560,7 @@ function RemoteReviewPage({ defaultBranch={defaultBranch} items={items} itemIndexByFilePath={itemIndexByFilePath} + commentedFilePaths={commentedFilePaths} currentSignatures={currentSignatures} viewedRecord={reviewState.viewedRecord} onToggleViewed={reviewState.toggleViewed} diff --git a/packages/ui/src/features/code-review/components/ReviewShell.tsx b/packages/ui/src/features/code-review/components/ReviewShell.tsx index 3e03271bfc..3a1362d5d2 100644 --- a/packages/ui/src/features/code-review/components/ReviewShell.tsx +++ b/packages/ui/src/features/code-review/components/ReviewShell.tsx @@ -5,7 +5,14 @@ import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTask import { useCloudPrUrl } from "@posthog/ui/features/git-interaction/useCloudPrUrl"; import { useTaskPrStatus } from "@posthog/ui/features/sidebar/useTaskPrStatus"; import { Flex, Spinner, Text } from "@radix-ui/themes"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { VList, type VListHandle } from "virtua"; import { REVIEW_LIST_BUFFER_PX, @@ -16,6 +23,8 @@ import { REVIEW_HOST, type ReviewHost } from "../reviewHost"; import { useReviewNavigationStore } from "../reviewNavigationStore"; import type { ReviewListItem, ReviewShellProps } from "../reviewShellParts"; import { + buildItemIndex, + filterReviewItemsByFilePaths, findActiveScrollKey, findRenderedScrollAnchor, isFileViewed, @@ -113,6 +122,7 @@ export function ReviewShell({ isEmpty, items, itemIndexByFilePath, + commentedFilePaths, currentSignatures, viewedRecord, onToggleViewed, @@ -135,6 +145,26 @@ export function ReviewShell({ const lastActiveRef = useRef(null); const pendingNavigationRef = useRef(null); const navigationFrameRef = useRef(null); + const [showCommentedFilesOnly, setShowCommentedFilesOnly] = useState(false); + const isCommentFilterActive = + showCommentedFilesOnly && commentedFilePaths !== undefined; + + const commentedItems = useMemo( + () => + commentedFilePaths + ? filterReviewItemsByFilePaths(items, commentedFilePaths) + : [], + [commentedFilePaths, items], + ); + const visibleItems = isCommentFilterActive ? commentedItems : items; + const visibleItemIndexByFilePath = useMemo( + () => buildItemIndex(visibleItems), + [visibleItems], + ); + const commentedFileCount = useMemo( + () => commentedItems.filter((item) => item.filePaths).length, + [commentedItems], + ); const workerFactory = useCallback( () => reviewHost.diffWorkerFactory(), @@ -147,12 +177,20 @@ export function ReviewShell({ const isExpanded = reviewMode === "expanded"; const viewedCount = useMemo(() => { + const visibleKeys = isCommentFilterActive + ? new Set( + visibleItems.flatMap((item) => + item.scrollKey ? [item.scrollKey] : [], + ), + ) + : null; let count = 0; for (const [key, sig] of currentSignatures) { + if (visibleKeys && !visibleKeys.has(key)) continue; if (isFileViewed(viewedRecord[key], sig)) count++; } return count; - }, [currentSignatures, viewedRecord]); + }, [currentSignatures, isCommentFilterActive, viewedRecord, visibleItems]); // Collapse already-viewed files on first open per task (mirrors GitHub). // Skips on re-opens: seededTaskRef prevents re-collapsing files the user @@ -219,8 +257,13 @@ export function ReviewShell({ useEffect(() => { if (!scrollRequest) return; - const targetIndex = itemIndexByFilePath.get(scrollRequest); - if (targetIndex === undefined) return; + const targetIndex = visibleItemIndexByFilePath.get(scrollRequest); + if (targetIndex === undefined) { + if (isCommentFilterActive && itemIndexByFilePath.has(scrollRequest)) { + setShowCommentedFilesOnly(false); + } + return; + } const currentSignature = currentSignatures.get(scrollRequest); const viewed = @@ -264,7 +307,9 @@ export function ReviewShell({ onUncollapseFile, scrollRequest, setActiveFilePath, + isCommentFilterActive, taskId, + visibleItemIndexByFilePath, viewedRecord, ]); @@ -293,6 +338,40 @@ export function ReviewShell({ [], ); + let reviewContent: ReactNode; + if (isLoading) { + reviewContent = ( + + + + ); + } else if (isEmpty || visibleItems.length === 0) { + reviewContent = ( + + + {isCommentFilterActive + ? "No files with comments" + : "No file changes to review"} + + + ); + } else { + reviewContent = ( + + {renderItem} + + ); + } + return ( setShowCommentedFilesOnly((current) => !current) + : undefined + } linesAdded={linesAdded} linesRemoved={linesRemoved} allExpanded={allExpanded} @@ -343,38 +429,7 @@ export function ReviewShell({ direction="column" className="min-w-0 flex-1" > - {isLoading ? ( - - - - ) : isEmpty ? ( - - - No file changes to review - - - ) : ( - - {renderItem} - - )} + {reviewContent} diff --git a/packages/ui/src/features/code-review/components/ReviewToolbar.tsx b/packages/ui/src/features/code-review/components/ReviewToolbar.tsx index f52d39e0d6..87c499852c 100644 --- a/packages/ui/src/features/code-review/components/ReviewToolbar.tsx +++ b/packages/ui/src/features/code-review/components/ReviewToolbar.tsx @@ -1,6 +1,7 @@ import { ArrowCounterClockwise, ArrowsClockwise, + ChatCircle, Columns, Rows, X, @@ -23,6 +24,9 @@ interface ReviewToolbarProps { taskId: string; fileCount: number; viewedCount: number; + commentedFileCount: number; + showCommentedFilesOnly: boolean; + onToggleCommentedFilesOnly?: () => void; linesAdded: number; linesRemoved: number; allExpanded: boolean; @@ -36,10 +40,18 @@ interface ReviewToolbarProps { defaultBranch?: string | null; } +function formatFileCount(count: number, suffix: string): string { + const noun = count === 1 ? "file" : "files"; + return `${count} ${noun} ${suffix}`; +} + export const ReviewToolbar = memo(function ReviewToolbar({ taskId, fileCount, viewedCount, + commentedFileCount, + showCommentedFilesOnly, + onToggleCommentedFilesOnly, allExpanded, onExpandAll, onCollapseAll, @@ -66,6 +78,13 @@ export const ReviewToolbar = memo(function ReviewToolbar({ setReviewMode(taskId, "closed"); }; + const visibleFileCount = showCommentedFilesOnly + ? commentedFileCount + : fileCount; + const fileCountLabel = showCommentedFilesOnly + ? formatFileCount(commentedFileCount, "with comments") + : formatFileCount(fileCount, "changed"); + return ( - - {fileCount} file{fileCount !== 1 ? "s" : ""} changed - - {fileCount > 0 && ( + {fileCountLabel} + {visibleFileCount > 0 && ( - {viewedCount}/{fileCount} viewed + {viewedCount}/{visibleFileCount} viewed )} {effectiveSource && ( @@ -98,6 +115,30 @@ export const ReviewToolbar = memo(function ReviewToolbar({ + {onToggleCommentedFilesOnly && ( + + + + )} + {onRefresh && ( + } + /> + - - - + Comment filter + + onCommentFilterChange(value as CommentFileFilter) + } + > + + All comments ({commentedFileCount}) + + + Unresolved comments ({unresolvedCommentedFileCount}) + + + {commentFilter !== "none" && ( + <> + + onCommentFilterChange("none")} + > + Clear comment filter + + + )} + + )} {onRefresh && ( diff --git a/packages/ui/src/features/code-review/reviewShellParts.test.tsx b/packages/ui/src/features/code-review/reviewShellParts.test.tsx index f42953db5c..afa3ac6476 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.test.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.test.tsx @@ -166,27 +166,40 @@ describe("review scroll anchors", () => { }); describe("commented file filtering", () => { - it("collects paths from threads containing comments", () => { + it("collects paths for all and unresolved comment threads", () => { const commentedPaths = getCommentedFilePaths( new Map([ [ 1, { filePath: "src/commented.ts", + isResolved: false, comments: [{ id: 1 }], }, ], [ 2, + { + filePath: "src/resolved.ts", + isResolved: true, + comments: [{ id: 2 }], + }, + ], + [ + 3, { filePath: "src/empty.ts", + isResolved: false, comments: [], }, ], ]) as Parameters[0], ); - expect(commentedPaths).toEqual(new Set(["src/commented.ts"])); + expect(commentedPaths).toEqual({ + all: new Set(["src/commented.ts", "src/resolved.ts"]), + unresolved: new Set(["src/commented.ts"]), + }); }); it("keeps matching files and their section headers", () => { diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index d2203a27fc..e849e24bf3 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -210,6 +210,7 @@ export interface ReviewShellProps { items: ReviewListItem[]; itemIndexByFilePath: Map; commentedFilePaths?: ReadonlySet; + unresolvedCommentedFilePaths?: ReadonlySet; currentSignatures: Map; viewedRecord: Record; onToggleViewed: (key: string, sig: string | null) => void; @@ -233,14 +234,20 @@ export interface ReviewListItem { node: ReactNode; } -export function getCommentedFilePaths( - threads: Map, -): Set { - return new Set( - [...threads.values()] - .filter((thread) => thread.comments.length > 0) - .map((thread) => thread.filePath), - ); +export function getCommentedFilePaths(threads: Map): { + all: Set; + unresolved: Set; +} { + const all = new Set(); + const unresolved = new Set(); + + for (const thread of threads.values()) { + if (thread.comments.length === 0) continue; + all.add(thread.filePath); + if (!thread.isResolved) unresolved.add(thread.filePath); + } + + return { all, unresolved }; } export function filterReviewItemsByFilePaths( From 3e75d53a8b516c92e1d7d44e4195dbe0ad55aed6 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Mon, 20 Jul 2026 11:44:18 -0400 Subject: [PATCH 04/10] refactor(code-review): move comment filter to settings Generated-By: PostHog Code Task-Id: 199c1147-fca6-415b-9ea1-00a45d9b7267 --- .../components/DiffSettingsMenu.tsx | 65 +++++++++++++++- .../code-review/components/ReviewShell.tsx | 3 +- .../code-review/components/ReviewToolbar.tsx | 77 ++----------------- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx b/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx index 3fca6ae674..f2644ede06 100644 --- a/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx +++ b/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx @@ -4,12 +4,31 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@posthog/quill"; import { useDiffViewerStore } from "@posthog/ui/features/code-editor/diffViewerStore"; -export function DiffSettingsMenu() { +export type CommentFileFilter = "none" | "commented" | "unresolved"; + +interface DiffSettingsMenuProps { + commentedFileCount: number; + unresolvedCommentedFileCount: number; + commentFilter: CommentFileFilter; + onCommentFilterChange?: (filter: CommentFileFilter) => void; +} + +export function DiffSettingsMenu({ + commentedFileCount, + unresolvedCommentedFileCount, + commentFilter, + onCommentFilterChange, +}: DiffSettingsMenuProps) { const wordWrap = useDiffViewerStore((s) => s.wordWrap); const toggleWordWrap = useDiffViewerStore((s) => s.toggleWordWrap); const wordDiffs = useDiffViewerStore((s) => s.wordDiffs); @@ -31,7 +50,12 @@ export function DiffSettingsMenu() { render={ - } - /> - - Comment filter - - onCommentFilterChange(value as CommentFileFilter) - } - > - - All comments ({commentedFileCount}) - - - Unresolved comments ({unresolvedCommentedFileCount}) - - - {commentFilter !== "none" && ( - <> - - onCommentFilterChange("none")} - > - Clear comment filter - - - )} - - - )} - {onRefresh && (