From 60396faa9eb5dd6a1f27b3b45c7b9a4bb66a480c Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 13:37:19 -0400 Subject: [PATCH 1/6] fix(code-review): show PR comment counts on files Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- .../components/PatchedFileDiff.tsx | 5 ++ .../components/ReviewRows.test.tsx | 67 +++++++++++++++++++ .../code-review/components/ReviewRows.tsx | 22 ++++++ .../code-review/prCommentThreads.test.ts | 60 +++++++++++++++++ .../features/code-review/prCommentThreads.ts | 19 ++++++ .../code-review/reviewShellParts.test.tsx | 15 ++++- .../features/code-review/reviewShellParts.tsx | 9 +++ .../features/git-interaction/usePrDetails.ts | 19 +----- 8 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/features/code-review/components/ReviewRows.test.tsx create mode 100644 packages/ui/src/features/code-review/prCommentThreads.test.ts create mode 100644 packages/ui/src/features/code-review/prCommentThreads.ts diff --git a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx index e648537e1d..e5b648e447 100644 --- a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx +++ b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx @@ -17,6 +17,7 @@ interface PatchedFileDiffProps { externalUrl?: string; prUrl?: string | null; commentThreads?: Map; + headerMetadata?: ReactNode; /** Extra controls in the file header row (e.g. a "Viewed" toggle). */ headerTrailing?: ReactNode; } @@ -31,6 +32,7 @@ export function PatchedFileDiff({ externalUrl, prUrl, commentThreads, + headerMetadata, headerTrailing, }: PatchedFileDiffProps) { const fileDiff = useMemo((): FileDiffMetadata | undefined => { @@ -63,6 +65,7 @@ export function PatchedFileDiff({ collapsed={collapsed} onToggle={onToggle} externalUrl={externalUrl} + headerMetadata={headerMetadata} headerTrailing={headerTrailing} /> ); @@ -78,6 +81,7 @@ export function PatchedFileDiff({ collapsed={collapsed} onToggle={onToggle} externalUrl={externalUrl} + headerMetadata={headerMetadata} headerTrailing={headerTrailing} /> ); @@ -95,6 +99,7 @@ export function PatchedFileDiff({ fileDiff={fd} collapsed={collapsed} onToggle={onToggle} + metadata={headerMetadata} trailing={headerTrailing} /> )} diff --git a/packages/ui/src/features/code-review/components/ReviewRows.test.tsx b/packages/ui/src/features/code-review/components/ReviewRows.test.tsx new file mode 100644 index 0000000000..aac64d1da0 --- /dev/null +++ b/packages/ui/src/features/code-review/components/ReviewRows.test.tsx @@ -0,0 +1,67 @@ +import type { PrCommentThread } from "@posthog/core/code-review/types"; +import type { ChangedFile } from "@posthog/shared/domain-types"; +import { render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("./PatchedFileDiff", () => ({ + PatchedFileDiff: ({ headerMetadata }: { headerMetadata?: ReactNode }) => ( +
{headerMetadata}
+ ), +})); + +import { RemoteRow } from "./ReviewRows"; + +const file = { + path: "src/reviewed.ts", + patch: "", + linesAdded: 1, + linesRemoved: 0, +} as ChangedFile; + +const commentThreads = new Map([ + [ + 1, + { + rootId: 1, + nodeId: "thread-1", + isResolved: false, + filePath: file.path, + comments: [{ id: 1 }, { id: 2 }] as PrCommentThread["comments"], + }, + ], +]); + +describe("RemoteRow", () => { + it("shows the comment count for a collapsed PR file", () => { + render( + {}} + commentThreads={commentThreads} + />, + ); + + expect(screen.getByTitle("2 comments")).toBeInTheDocument(); + }); + + it("shows the comment count when the file is expanded", () => { + render( + {}} + commentThreads={commentThreads} + />, + ); + + expect(screen.getByTitle("2 comments")).toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/features/code-review/components/ReviewRows.tsx b/packages/ui/src/features/code-review/components/ReviewRows.tsx index d789e4f0e7..a676f69168 100644 --- a/packages/ui/src/features/code-review/components/ReviewRows.tsx +++ b/packages/ui/src/features/code-review/components/ReviewRows.tsx @@ -1,3 +1,4 @@ +import { ChatCircleIcon } from "@phosphor-icons/react"; import type { parsePatchFiles } from "@pierre/diffs"; import { contentHash } from "@posthog/core/code-review/contentHash"; import type { PrCommentThread } from "@posthog/core/code-review/types"; @@ -7,6 +8,7 @@ import { memo, useCallback, useMemo } from "react"; import { useInView } from "../../../primitives/hooks/useInView"; import { REVIEW_PREFETCH_ROOT_MARGIN } from "../constants"; import { useReadRepoFileBounded } from "../hooks/useReadRepoFileBounded"; +import { countPrCommentsForFile } from "../prCommentThreads"; import { DeferredDiffPlaceholder, DiffFileHeader, @@ -205,6 +207,7 @@ export const RemoteRow = memo(function RemoteRow({ () => toggleFile(file.path), [toggleFile, file.path], ); + const commentCount = countPrCommentsForFile(commentThreads, file.path); return ( 0 ? ( + + ) : undefined + } /> ); }); +function PrCommentCountBadge({ count }: { count: number }) { + const label = `${count} comment${count === 1 ? "" : "s"}`; + return ( + + + {count} + comment{count === 1 ? "" : "s"} + + ); +} + function UntrackedFileDiff({ file, repoPath, diff --git a/packages/ui/src/features/code-review/prCommentThreads.test.ts b/packages/ui/src/features/code-review/prCommentThreads.test.ts new file mode 100644 index 0000000000..da45aa6b4e --- /dev/null +++ b/packages/ui/src/features/code-review/prCommentThreads.test.ts @@ -0,0 +1,60 @@ +import type { PrReviewThread } from "@posthog/shared"; +import { describe, expect, it } from "vitest"; +import { + countPrCommentsForFile, + mapPrCommentThreads, +} from "./prCommentThreads"; + +function makeThread( + rootId: number, + filePath: string, + commentCount: number, +): PrReviewThread { + return { + rootId, + nodeId: `thread-${rootId}`, + isResolved: false, + filePath, + comments: Array.from({ length: commentCount }, (_, index) => ({ + id: rootId * 100 + index, + body: "comment", + path: filePath, + line: 1, + original_line: 1, + side: "RIGHT" as const, + start_line: null, + start_side: null, + diff_hunk: "@@ -1 +1 @@", + in_reply_to_id: null, + user: { login: "reviewer", avatar_url: "" }, + created_at: "2026-07-14T00:00:00Z", + updated_at: "2026-07-14T00:00:00Z", + subject_type: "line" as const, + })), + }; +} + +describe("PR comment thread helpers", () => { + it("maps threads by root id", () => { + const threads = [ + makeThread(1, "src/one.ts", 1), + makeThread(2, "src/two.ts", 1), + ]; + + expect([...mapPrCommentThreads(threads).keys()]).toEqual([1, 2]); + }); + + it.each([ + ["src/one.ts", 3], + ["src/two.ts", 4], + ["src/missing.ts", 0], + ])("counts all comments and replies for %s", (filePath, expected) => { + const threads = mapPrCommentThreads([ + makeThread(1, "src/one.ts", 2), + makeThread(2, "src/one.ts", 1), + makeThread(3, "src/two.ts", 4), + ]); + + expect(countPrCommentsForFile(threads, filePath)).toBe(expected); + }); +}); diff --git a/packages/ui/src/features/code-review/prCommentThreads.ts b/packages/ui/src/features/code-review/prCommentThreads.ts new file mode 100644 index 0000000000..78b3af5177 --- /dev/null +++ b/packages/ui/src/features/code-review/prCommentThreads.ts @@ -0,0 +1,19 @@ +import type { PrCommentThread } from "@posthog/core/code-review/types"; +import type { PrReviewThread } from "@posthog/shared"; + +export function mapPrCommentThreads( + threads: PrReviewThread[], +): Map { + return new Map(threads.map((thread) => [thread.rootId, thread])); +} + +export function countPrCommentsForFile( + threads: Map | undefined, + filePath: string, +): number { + let count = 0; + for (const thread of threads?.values() ?? []) { + if (thread.filePath === filePath) count += thread.comments.length; + } + return count; +} diff --git a/packages/ui/src/features/code-review/reviewShellParts.test.tsx b/packages/ui/src/features/code-review/reviewShellParts.test.tsx index 2361dae47a..5a3c796630 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.test.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.test.tsx @@ -36,12 +36,13 @@ function findSpan( return found; } -function renderHeader(path: string) { +function renderHeader(path: string, metadata?: React.ReactNode) { const diff = render( {}} + metadata={metadata} />, ); const deferred = render( @@ -52,6 +53,7 @@ function renderHeader(path: string) { reason="line-limit" collapsed={false} onToggle={() => {}} + headerMetadata={metadata} />, ); return { diff, deferred }; @@ -96,4 +98,15 @@ describe.each([ expect(dirSpan.parentElement).toBe(fileSpan.parentElement); expect(dirSpan.parentElement?.classList.contains("flex")).toBe(true); }); + + it("renders metadata before line changes", () => { + const rendered = renderHeader( + "src/ReviewShell.tsx", + 2 comments, + )[which]; + const text = rendered.container.querySelector("button")?.textContent ?? ""; + const additions = which === "diff" ? "+3" : "+10"; + + expect(text.indexOf("2 comments")).toBeLessThan(text.indexOf(additions)); + }); }); diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index cafe7f1ceb..9bd42d615e 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -139,6 +139,7 @@ export function FileHeaderRow({ deletions, collapsed, onToggle, + metadata, trailing, }: { dirPath: string; @@ -147,6 +148,7 @@ export function FileHeaderRow({ deletions: number; collapsed: boolean; onToggle: () => void; + metadata?: ReactNode; trailing?: ReactNode; }) { return ( @@ -176,6 +178,7 @@ export function FileHeaderRow({ {dirPath} + {metadata} {additions > 0 && ( +{additions} @@ -210,6 +213,7 @@ export function DiffFileHeader({ onDiscard, onStage, staged, + metadata, trailing, }: { fileDiff: FileDiffMetadata; @@ -219,6 +223,7 @@ export function DiffFileHeader({ onDiscard?: () => void; onStage?: () => void; staged?: boolean; + metadata?: ReactNode; /** Extra controls rendered after the action buttons (e.g. a "Viewed" toggle). */ trailing?: ReactNode; }) { @@ -237,6 +242,7 @@ export function DiffFileHeader({ deletions={deletions} collapsed={collapsed} onToggle={onToggle} + metadata={metadata} trailing={ (onStage || onDiscard || onOpenFile || trailing) && ( @@ -299,6 +305,7 @@ export function DeferredDiffPlaceholder({ onToggle, onShow, externalUrl, + headerMetadata, headerTrailing, }: { filePath: string; @@ -309,6 +316,7 @@ export function DeferredDiffPlaceholder({ onToggle: () => void; onShow?: () => void; externalUrl?: string; + headerMetadata?: ReactNode; /** Extra controls in the header row (e.g. a "Viewed" toggle). */ headerTrailing?: ReactNode; }) { @@ -323,6 +331,7 @@ export function DeferredDiffPlaceholder({ deletions={linesRemoved} collapsed={collapsed} onToggle={onToggle} + metadata={headerMetadata} trailing={ headerTrailing && ( diff --git a/packages/ui/src/features/git-interaction/usePrDetails.ts b/packages/ui/src/features/git-interaction/usePrDetails.ts index 657b027290..96d5ca388b 100644 --- a/packages/ui/src/features/git-interaction/usePrDetails.ts +++ b/packages/ui/src/features/git-interaction/usePrDetails.ts @@ -1,27 +1,12 @@ import { useHostTRPC } from "@posthog/host-router/react"; -import type { PrReviewThread } from "@posthog/shared"; import { useQueries, useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -import type { PrCommentThread } from "../code-review/prCommentAnnotations"; +import { mapPrCommentThreads } from "../code-review/prCommentThreads"; interface UsePrDetailsOptions { includeComments?: boolean; } -function threadsToMap(threads: PrReviewThread[]): Map { - const map = new Map(); - for (const thread of threads) { - map.set(thread.rootId, { - rootId: thread.rootId, - nodeId: thread.nodeId, - isResolved: thread.isResolved, - comments: thread.comments, - filePath: thread.filePath, - }); - } - return map; -} - export interface PrStateDetails { state: string; merged: boolean; @@ -80,7 +65,7 @@ export function usePrDetails( }); const commentThreads = useMemo( - () => threadsToMap(commentsQuery.data ?? []), + () => mapPrCommentThreads(commentsQuery.data ?? []), [commentsQuery.data], ); From f0dc203816fd3f9fbab4017844566c03950d89f3 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 13:54:55 -0400 Subject: [PATCH 2/6] test(code-review): cover PR header metadata paths Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- .../components/PatchedFileDiff.test.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx diff --git a/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx b/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx new file mode 100644 index 0000000000..e4f72d7823 --- /dev/null +++ b/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx @@ -0,0 +1,61 @@ +import type { FileDiffMetadata } from "@pierre/diffs"; +import type { ChangedFile } from "@posthog/shared/domain-types"; +import { render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../../primitives/FileIcon", () => ({ + FileIcon: () => , +})); + +vi.mock("./InteractiveFileDiff", () => ({ + InteractiveFileDiff: ({ + fileDiff, + renderCustomHeader, + }: { + fileDiff: FileDiffMetadata; + renderCustomHeader: (fileDiff: FileDiffMetadata) => ReactNode; + }) => renderCustomHeader(fileDiff), +})); + +import { PatchedFileDiff } from "./PatchedFileDiff"; + +const patch = `diff --git a/src/reviewed.ts b/src/reviewed.ts +index 1111111..2222222 100644 +--- a/src/reviewed.ts ++++ b/src/reviewed.ts +@@ -1 +1 @@ +-before ++after`; + +describe.each([ + ["regular", { path: "src/reviewed.ts", patch }], + ["binary", { path: "assets/reviewed.png", patch: null }], + ["unavailable", { path: "src/unavailable.ts", patch: null }], +] as const)("PatchedFileDiff %s header", (_kind, fileInput) => { + it("renders metadata before line change stats", () => { + const file = { + ...fileInput, + linesAdded: 2, + linesRemoved: 1, + } as ChangedFile; + + render( + {}} + headerMetadata={2 comments} + />, + ); + + const header = screen.getByRole("button"); + const text = header.textContent ?? ""; + const additions = _kind === "regular" ? "+1" : "+2"; + + expect(screen.getByText("2 comments")).toBeInTheDocument(); + expect(text.indexOf("2 comments")).toBeLessThan(text.indexOf(additions)); + }); +}); From 5ad613ee3c76f1a98b3cdc3132fcc754eb44426c Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 13:59:15 -0400 Subject: [PATCH 3/6] fix(code-review): centralize PR file comment badges Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- .../components/PatchedFileDiff.test.tsx | 27 +++++++- .../components/PatchedFileDiff.tsx | 10 +-- .../components/ReviewRows.test.tsx | 67 ------------------- .../code-review/components/ReviewRows.tsx | 22 ------ .../code-review/prCommentThreads.test.ts | 11 +-- .../features/code-review/prCommentThreads.ts | 10 ++- .../code-review/reviewShellParts.test.tsx | 11 ++- .../features/code-review/reviewShellParts.tsx | 35 +++++++--- 8 files changed, 73 insertions(+), 120 deletions(-) delete mode 100644 packages/ui/src/features/code-review/components/ReviewRows.test.tsx diff --git a/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx b/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx index e4f72d7823..bc2fac4499 100644 --- a/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx +++ b/packages/ui/src/features/code-review/components/PatchedFileDiff.test.tsx @@ -1,4 +1,5 @@ import type { FileDiffMetadata } from "@pierre/diffs"; +import type { PrCommentThread } from "@posthog/core/code-review/types"; import type { ChangedFile } from "@posthog/shared/domain-types"; import { render, screen } from "@testing-library/react"; import type { ReactNode } from "react"; @@ -29,7 +30,14 @@ index 1111111..2222222 100644 +after`; describe.each([ - ["regular", { path: "src/reviewed.ts", patch }], + [ + "regular", + { + path: "src/reviewed.ts", + originalPath: "src/original.ts", + patch, + }, + ], ["binary", { path: "assets/reviewed.png", patch: null }], ["unavailable", { path: "src/unavailable.ts", patch: null }], ] as const)("PatchedFileDiff %s header", (_kind, fileInput) => { @@ -39,6 +47,19 @@ describe.each([ linesAdded: 2, linesRemoved: 1, } as ChangedFile; + const threadPath = file.originalPath ?? file.path; + const commentThreads = new Map([ + [ + 1, + { + rootId: 1, + nodeId: "thread-1", + isResolved: false, + filePath: threadPath, + comments: [{ id: 1 }, { id: 2 }] as PrCommentThread["comments"], + }, + ], + ]); render( {}} - headerMetadata={2 comments} + commentThreads={commentThreads} />, ); @@ -55,7 +76,7 @@ describe.each([ const text = header.textContent ?? ""; const additions = _kind === "regular" ? "+1" : "+2"; - expect(screen.getByText("2 comments")).toBeInTheDocument(); + expect(screen.getByTitle("2 comments")).toBeInTheDocument(); expect(text.indexOf("2 comments")).toBeLessThan(text.indexOf(additions)); }); }); diff --git a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx index e5b648e447..1b97866d96 100644 --- a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx +++ b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx @@ -3,6 +3,7 @@ import type { PrCommentThread } from "@posthog/core/code-review/types"; import { isBinaryFile } from "@posthog/shared"; import type { ChangedFile } from "@posthog/shared/domain-types"; import { type ReactNode, useMemo } from "react"; +import { countPrCommentsForFile } from "../prCommentThreads"; import { DeferredDiffPlaceholder, DiffFileHeader } from "../reviewShellParts"; import type { DiffOptions } from "../types"; import { InteractiveFileDiff } from "./InteractiveFileDiff"; @@ -17,7 +18,6 @@ interface PatchedFileDiffProps { externalUrl?: string; prUrl?: string | null; commentThreads?: Map; - headerMetadata?: ReactNode; /** Extra controls in the file header row (e.g. a "Viewed" toggle). */ headerTrailing?: ReactNode; } @@ -32,7 +32,6 @@ export function PatchedFileDiff({ externalUrl, prUrl, commentThreads, - headerMetadata, headerTrailing, }: PatchedFileDiffProps) { const fileDiff = useMemo((): FileDiffMetadata | undefined => { @@ -51,6 +50,7 @@ export function PatchedFileDiff({ } return null; }, [fileDiff, fallback, file.path]); + const commentCount = countPrCommentsForFile(commentThreads, file); // Branch/PR diffs have no reliable local working-tree file to preview (the // checkout may be on a different ref, and GitHub omits binary patches), so @@ -65,7 +65,7 @@ export function PatchedFileDiff({ collapsed={collapsed} onToggle={onToggle} externalUrl={externalUrl} - headerMetadata={headerMetadata} + commentCount={commentCount} headerTrailing={headerTrailing} /> ); @@ -81,7 +81,7 @@ export function PatchedFileDiff({ collapsed={collapsed} onToggle={onToggle} externalUrl={externalUrl} - headerMetadata={headerMetadata} + commentCount={commentCount} headerTrailing={headerTrailing} /> ); @@ -99,7 +99,7 @@ export function PatchedFileDiff({ fileDiff={fd} collapsed={collapsed} onToggle={onToggle} - metadata={headerMetadata} + commentCount={commentCount} trailing={headerTrailing} /> )} diff --git a/packages/ui/src/features/code-review/components/ReviewRows.test.tsx b/packages/ui/src/features/code-review/components/ReviewRows.test.tsx deleted file mode 100644 index aac64d1da0..0000000000 --- a/packages/ui/src/features/code-review/components/ReviewRows.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import type { PrCommentThread } from "@posthog/core/code-review/types"; -import type { ChangedFile } from "@posthog/shared/domain-types"; -import { render, screen } from "@testing-library/react"; -import type { ReactNode } from "react"; -import { describe, expect, it, vi } from "vitest"; - -vi.mock("./PatchedFileDiff", () => ({ - PatchedFileDiff: ({ headerMetadata }: { headerMetadata?: ReactNode }) => ( -
{headerMetadata}
- ), -})); - -import { RemoteRow } from "./ReviewRows"; - -const file = { - path: "src/reviewed.ts", - patch: "", - linesAdded: 1, - linesRemoved: 0, -} as ChangedFile; - -const commentThreads = new Map([ - [ - 1, - { - rootId: 1, - nodeId: "thread-1", - isResolved: false, - filePath: file.path, - comments: [{ id: 1 }, { id: 2 }] as PrCommentThread["comments"], - }, - ], -]); - -describe("RemoteRow", () => { - it("shows the comment count for a collapsed PR file", () => { - render( - {}} - commentThreads={commentThreads} - />, - ); - - expect(screen.getByTitle("2 comments")).toBeInTheDocument(); - }); - - it("shows the comment count when the file is expanded", () => { - render( - {}} - commentThreads={commentThreads} - />, - ); - - expect(screen.getByTitle("2 comments")).toBeInTheDocument(); - }); -}); diff --git a/packages/ui/src/features/code-review/components/ReviewRows.tsx b/packages/ui/src/features/code-review/components/ReviewRows.tsx index a676f69168..d789e4f0e7 100644 --- a/packages/ui/src/features/code-review/components/ReviewRows.tsx +++ b/packages/ui/src/features/code-review/components/ReviewRows.tsx @@ -1,4 +1,3 @@ -import { ChatCircleIcon } from "@phosphor-icons/react"; import type { parsePatchFiles } from "@pierre/diffs"; import { contentHash } from "@posthog/core/code-review/contentHash"; import type { PrCommentThread } from "@posthog/core/code-review/types"; @@ -8,7 +7,6 @@ import { memo, useCallback, useMemo } from "react"; import { useInView } from "../../../primitives/hooks/useInView"; import { REVIEW_PREFETCH_ROOT_MARGIN } from "../constants"; import { useReadRepoFileBounded } from "../hooks/useReadRepoFileBounded"; -import { countPrCommentsForFile } from "../prCommentThreads"; import { DeferredDiffPlaceholder, DiffFileHeader, @@ -207,7 +205,6 @@ export const RemoteRow = memo(function RemoteRow({ () => toggleFile(file.path), [toggleFile, file.path], ); - const commentCount = countPrCommentsForFile(commentThreads, file.path); return ( 0 ? ( - - ) : undefined - } /> ); }); -function PrCommentCountBadge({ count }: { count: number }) { - const label = `${count} comment${count === 1 ? "" : "s"}`; - return ( - - - {count} - comment{count === 1 ? "" : "s"} - - ); -} - function UntrackedFileDiff({ file, repoPath, diff --git a/packages/ui/src/features/code-review/prCommentThreads.test.ts b/packages/ui/src/features/code-review/prCommentThreads.test.ts index da45aa6b4e..24c3f76942 100644 --- a/packages/ui/src/features/code-review/prCommentThreads.test.ts +++ b/packages/ui/src/features/code-review/prCommentThreads.test.ts @@ -45,16 +45,17 @@ describe("PR comment thread helpers", () => { }); it.each([ - ["src/one.ts", 3], - ["src/two.ts", 4], - ["src/missing.ts", 0], - ])("counts all comments and replies for %s", (filePath, expected) => { + [{ path: "src/one.ts" }, 3], + [{ path: "src/two.ts" }, 4], + [{ path: "src/renamed.ts", originalPath: "src/one.ts" }, 3], + [{ path: "src/missing.ts" }, 0], + ])("counts all comments and replies for $0", (file, expected) => { const threads = mapPrCommentThreads([ makeThread(1, "src/one.ts", 2), makeThread(2, "src/one.ts", 1), makeThread(3, "src/two.ts", 4), ]); - expect(countPrCommentsForFile(threads, filePath)).toBe(expected); + expect(countPrCommentsForFile(threads, file)).toBe(expected); }); }); diff --git a/packages/ui/src/features/code-review/prCommentThreads.ts b/packages/ui/src/features/code-review/prCommentThreads.ts index 78b3af5177..c503662568 100644 --- a/packages/ui/src/features/code-review/prCommentThreads.ts +++ b/packages/ui/src/features/code-review/prCommentThreads.ts @@ -1,5 +1,6 @@ import type { PrCommentThread } from "@posthog/core/code-review/types"; import type { PrReviewThread } from "@posthog/shared"; +import type { ChangedFile } from "@posthog/shared/domain-types"; export function mapPrCommentThreads( threads: PrReviewThread[], @@ -9,11 +10,16 @@ export function mapPrCommentThreads( export function countPrCommentsForFile( threads: Map | undefined, - filePath: string, + file: Pick, ): number { let count = 0; for (const thread of threads?.values() ?? []) { - if (thread.filePath === filePath) count += thread.comments.length; + if ( + thread.filePath === file.path || + (file.originalPath != null && thread.filePath === file.originalPath) + ) { + count += thread.comments.length; + } } return count; } diff --git a/packages/ui/src/features/code-review/reviewShellParts.test.tsx b/packages/ui/src/features/code-review/reviewShellParts.test.tsx index 5a3c796630..a07d37b55b 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.test.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.test.tsx @@ -36,13 +36,13 @@ function findSpan( return found; } -function renderHeader(path: string, metadata?: React.ReactNode) { +function renderHeader(path: string, commentCount?: number) { const diff = render( {}} - metadata={metadata} + commentCount={commentCount} />, ); const deferred = render( @@ -53,7 +53,7 @@ function renderHeader(path: string, metadata?: React.ReactNode) { reason="line-limit" collapsed={false} onToggle={() => {}} - headerMetadata={metadata} + commentCount={commentCount} />, ); return { diff, deferred }; @@ -100,10 +100,7 @@ describe.each([ }); it("renders metadata before line changes", () => { - const rendered = renderHeader( - "src/ReviewShell.tsx", - 2 comments, - )[which]; + const rendered = renderHeader("src/ReviewShell.tsx", 2)[which]; const text = rendered.container.querySelector("button")?.textContent ?? ""; const additions = which === "diff" ? "+3" : "+10"; diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index 9bd42d615e..8b65943ca0 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -2,6 +2,7 @@ import { ArrowCounterClockwise, ArrowSquareOut, CaretDown, + ChatCircle, Minus, Plus, } from "@phosphor-icons/react"; @@ -139,7 +140,7 @@ export function FileHeaderRow({ deletions, collapsed, onToggle, - metadata, + commentCount, trailing, }: { dirPath: string; @@ -148,7 +149,7 @@ export function FileHeaderRow({ deletions: number; collapsed: boolean; onToggle: () => void; - metadata?: ReactNode; + commentCount?: number; trailing?: ReactNode; }) { return ( @@ -178,7 +179,9 @@ export function FileHeaderRow({ {dirPath}
- {metadata} + {commentCount != null && commentCount > 0 && ( + + )} {additions > 0 && ( +{additions} @@ -213,7 +216,7 @@ export function DiffFileHeader({ onDiscard, onStage, staged, - metadata, + commentCount, trailing, }: { fileDiff: FileDiffMetadata; @@ -223,7 +226,7 @@ export function DiffFileHeader({ onDiscard?: () => void; onStage?: () => void; staged?: boolean; - metadata?: ReactNode; + commentCount?: number; /** Extra controls rendered after the action buttons (e.g. a "Viewed" toggle). */ trailing?: ReactNode; }) { @@ -242,7 +245,7 @@ export function DiffFileHeader({ deletions={deletions} collapsed={collapsed} onToggle={onToggle} - metadata={metadata} + commentCount={commentCount} trailing={ (onStage || onDiscard || onOpenFile || trailing) && ( @@ -305,7 +308,7 @@ export function DeferredDiffPlaceholder({ onToggle, onShow, externalUrl, - headerMetadata, + commentCount, headerTrailing, }: { filePath: string; @@ -316,7 +319,7 @@ export function DeferredDiffPlaceholder({ onToggle: () => void; onShow?: () => void; externalUrl?: string; - headerMetadata?: ReactNode; + commentCount?: number; /** Extra controls in the header row (e.g. a "Viewed" toggle). */ headerTrailing?: ReactNode; }) { @@ -331,7 +334,7 @@ export function DeferredDiffPlaceholder({ deletions={linesRemoved} collapsed={collapsed} onToggle={onToggle} - metadata={headerMetadata} + commentCount={commentCount} trailing={ headerTrailing && ( @@ -378,3 +381,17 @@ export function DeferredDiffPlaceholder({ ); } + +function PrCommentCountBadge({ count }: { count: number }) { + const label = `${count} comment${count === 1 ? "" : "s"}`; + return ( + + + {count} + comment{count === 1 ? "" : "s"} + + ); +} From 621e92405e1c92bfeb16bdd75545ad90efd3c856 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 14:03:03 -0400 Subject: [PATCH 4/6] refactor(code-review): simplify PR comment badges Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- .../components/PatchedFileDiff.tsx | 17 +++++- .../code-review/prCommentThreads.test.ts | 61 ------------------- .../features/code-review/prCommentThreads.ts | 25 -------- .../features/code-review/reviewShellParts.tsx | 8 ++- .../features/git-interaction/usePrDetails.ts | 9 ++- 5 files changed, 29 insertions(+), 91 deletions(-) delete mode 100644 packages/ui/src/features/code-review/prCommentThreads.test.ts delete mode 100644 packages/ui/src/features/code-review/prCommentThreads.ts diff --git a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx index 1b97866d96..93b987636a 100644 --- a/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx +++ b/packages/ui/src/features/code-review/components/PatchedFileDiff.tsx @@ -3,7 +3,6 @@ import type { PrCommentThread } from "@posthog/core/code-review/types"; import { isBinaryFile } from "@posthog/shared"; import type { ChangedFile } from "@posthog/shared/domain-types"; import { type ReactNode, useMemo } from "react"; -import { countPrCommentsForFile } from "../prCommentThreads"; import { DeferredDiffPlaceholder, DiffFileHeader } from "../reviewShellParts"; import type { DiffOptions } from "../types"; import { InteractiveFileDiff } from "./InteractiveFileDiff"; @@ -106,3 +105,19 @@ export function PatchedFileDiff({ /> ); } + +function countPrCommentsForFile( + threads: Map | undefined, + file: Pick, +): number { + let count = 0; + for (const thread of threads?.values() ?? []) { + if ( + thread.filePath === file.path || + (file.originalPath != null && thread.filePath === file.originalPath) + ) { + count += thread.comments.length; + } + } + return count; +} diff --git a/packages/ui/src/features/code-review/prCommentThreads.test.ts b/packages/ui/src/features/code-review/prCommentThreads.test.ts deleted file mode 100644 index 24c3f76942..0000000000 --- a/packages/ui/src/features/code-review/prCommentThreads.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { PrReviewThread } from "@posthog/shared"; -import { describe, expect, it } from "vitest"; -import { - countPrCommentsForFile, - mapPrCommentThreads, -} from "./prCommentThreads"; - -function makeThread( - rootId: number, - filePath: string, - commentCount: number, -): PrReviewThread { - return { - rootId, - nodeId: `thread-${rootId}`, - isResolved: false, - filePath, - comments: Array.from({ length: commentCount }, (_, index) => ({ - id: rootId * 100 + index, - body: "comment", - path: filePath, - line: 1, - original_line: 1, - side: "RIGHT" as const, - start_line: null, - start_side: null, - diff_hunk: "@@ -1 +1 @@", - in_reply_to_id: null, - user: { login: "reviewer", avatar_url: "" }, - created_at: "2026-07-14T00:00:00Z", - updated_at: "2026-07-14T00:00:00Z", - subject_type: "line" as const, - })), - }; -} - -describe("PR comment thread helpers", () => { - it("maps threads by root id", () => { - const threads = [ - makeThread(1, "src/one.ts", 1), - makeThread(2, "src/two.ts", 1), - ]; - - expect([...mapPrCommentThreads(threads).keys()]).toEqual([1, 2]); - }); - - it.each([ - [{ path: "src/one.ts" }, 3], - [{ path: "src/two.ts" }, 4], - [{ path: "src/renamed.ts", originalPath: "src/one.ts" }, 3], - [{ path: "src/missing.ts" }, 0], - ])("counts all comments and replies for $0", (file, expected) => { - const threads = mapPrCommentThreads([ - makeThread(1, "src/one.ts", 2), - makeThread(2, "src/one.ts", 1), - makeThread(3, "src/two.ts", 4), - ]); - - expect(countPrCommentsForFile(threads, file)).toBe(expected); - }); -}); diff --git a/packages/ui/src/features/code-review/prCommentThreads.ts b/packages/ui/src/features/code-review/prCommentThreads.ts deleted file mode 100644 index c503662568..0000000000 --- a/packages/ui/src/features/code-review/prCommentThreads.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { PrCommentThread } from "@posthog/core/code-review/types"; -import type { PrReviewThread } from "@posthog/shared"; -import type { ChangedFile } from "@posthog/shared/domain-types"; - -export function mapPrCommentThreads( - threads: PrReviewThread[], -): Map { - return new Map(threads.map((thread) => [thread.rootId, thread])); -} - -export function countPrCommentsForFile( - threads: Map | undefined, - file: Pick, -): number { - let count = 0; - for (const thread of threads?.values() ?? []) { - if ( - thread.filePath === file.path || - (file.originalPath != null && thread.filePath === file.originalPath) - ) { - count += thread.comments.length; - } - } - return count; -} diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index 8b65943ca0..86d84c4a58 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -14,6 +14,7 @@ import { splitFilePath, sumHunkStats, } from "@posthog/core/code-review/reviewShellGeometry"; +import { Badge } from "@posthog/quill"; import type { ChangedFile, Task } from "@posthog/shared/domain-types"; import { type ReactNode, useCallback, useMemo, useState } from "react"; import { FileIcon } from "../../primitives/FileIcon"; @@ -385,13 +386,14 @@ export function DeferredDiffPlaceholder({ function PrCommentCountBadge({ count }: { count: number }) { const label = `${count} comment${count === 1 ? "" : "s"}`; return ( - {count} comment{count === 1 ? "" : "s"} - + ); } diff --git a/packages/ui/src/features/git-interaction/usePrDetails.ts b/packages/ui/src/features/git-interaction/usePrDetails.ts index 96d5ca388b..270309103b 100644 --- a/packages/ui/src/features/git-interaction/usePrDetails.ts +++ b/packages/ui/src/features/git-interaction/usePrDetails.ts @@ -1,12 +1,19 @@ import { useHostTRPC } from "@posthog/host-router/react"; +import type { PrReviewThread } from "@posthog/shared"; import { useQueries, useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -import { mapPrCommentThreads } from "../code-review/prCommentThreads"; +import type { PrCommentThread } from "../code-review/prCommentAnnotations"; interface UsePrDetailsOptions { includeComments?: boolean; } +function mapPrCommentThreads( + threads: PrReviewThread[], +): Map { + return new Map(threads.map((thread) => [thread.rootId, thread])); +} + export interface PrStateDetails { state: string; merged: boolean; From 5c387c8fb4101b24d69e67de65757833f904ad5d Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 14:06:35 -0400 Subject: [PATCH 5/6] style(code-review): use neutral comment badge Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- packages/ui/src/features/code-review/reviewShellParts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index 86d84c4a58..2edb938b04 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -387,7 +387,7 @@ function PrCommentCountBadge({ count }: { count: number }) { const label = `${count} comment${count === 1 ? "" : "s"}`; return ( From a6cce556b8d47530898884b579d64a1dc372dae0 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 14:08:42 -0400 Subject: [PATCH 6/6] style(code-review): increase comment badge contrast Generated-By: PostHog Code Task-Id: 2e4a7ce2-5055-4c59-99c9-6daed60688c4 --- packages/ui/src/features/code-review/reviewShellParts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/features/code-review/reviewShellParts.tsx b/packages/ui/src/features/code-review/reviewShellParts.tsx index 2edb938b04..ae283f7e92 100644 --- a/packages/ui/src/features/code-review/reviewShellParts.tsx +++ b/packages/ui/src/features/code-review/reviewShellParts.tsx @@ -389,7 +389,7 @@ function PrCommentCountBadge({ count }: { count: number }) { {count}