authorgravatar for steva.borus@gmail.comStevan Borus <steva.borus@gmail.com> 2026-10-06 22:56:33+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 13:56:33-07:00
logf21d6da51c9aa78ea12e7f13a378f55f1e63cb38
tree0e6091e41a5a81f4246a4dae7867eab186636f46
parent115640ae49924c9e4e6215bd8d911445ad932bff
signature Signed by PGP key B5690EEEBB952194

perf(web): diff panel no longer re-renders every file header each time a patch arrives (#16033)

Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

3 files changed, 145 insertions(+), 79 deletions(-)

apps/web/src/components/DiffPanel.tsx+103-54
......@@ -21,7 +21,7 @@ import {
2121import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide";
2222import * as Schema from "effect/Schema";
2323import * as DateTime from "effect/DateTime";
24import { useCallback, useEffect, useMemo, useRef, useState } from "react";
24import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2525import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal";
2626import { useOpenInPreferredEditor } from "../editorPreferences";
2727import { useFileContextMenuHandler } from "../fileContextMenu";
......@@ -118,6 +118,74 @@ interface CollapsedDiffFilesState {
118118
119119const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set();
120120
121/** Collapse control for one file header; re-renders only when its own file changes. */
122function DiffFileCollapseToggle({
123 filePath,
124 fileKey,
125 collapsed,
126 unavailable,
127 iconClassName,
128 onToggle,
129}: {
130 filePath: string;
131 fileKey: string;
132 collapsed: boolean;
133 unavailable: boolean;
134 iconClassName: string;
135 onToggle: (fileKey: string) => void;
136}) {
137 return (
138 <Tooltip>
139 <TooltipTrigger
140 render={
141 <Button
142 size="icon-micro"
143 variant="ghost"
144 className="-ms-0.5"
145 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}
146 aria-expanded={!collapsed}
147 disabled={unavailable}
148 onClick={(event) => {
149 event.stopPropagation();
150 onToggle(fileKey);
151 }}
152 />
153 }
154 >
155 <MorphIcon
156 className={cn("size-4", iconClassName)}
157 icon={collapsed ? ChevronRight : ChevronDown}
158 />
159 </TooltipTrigger>
160 <TooltipPopup side="top">{collapsed ? "Expand diff" : "Collapse diff"}</TooltipPopup>
161 </Tooltip>
162 );
163}
164
165/** Copy and status controls for one file header; re-renders only when its own file changes. */
166function DiffFileHeaderSuffix({
167 filePath,
168 hasStat,
169 error,
170 truncated,
171 onRetry,
172}: {
173 filePath: string;
174 hasStat: boolean;
175 error: boolean;
176 truncated: boolean;
177 onRetry: (path: string) => void;
178}) {
179 return (
180 <>
181 <DiffFilePathCopyButton filePath={filePath} />
182 {hasStat ? (
183 <DiffFileStatus error={error} truncated={truncated} retry={() => onRetry(filePath)} />
184 ) : null}
185 </>
186 );
187}
188
121189interface DiffPanelProps {
122190 mode?: DiffPanelMode;
123191 composerDraftTarget: ScopedThreadRef | DraftId;
......@@ -598,22 +666,24 @@ export default function DiffPanel({
598666 },
599667 [activeCwd, activeRepositoryRoot, openInPreferredEditor, routeThreadRef],
600668 );
601 const toggleDiffFileCollapsed = useCallback(
602 (fileKey: string) => {
603 setCollapsedDiffFiles((current) => {
604 const next = new Set(
605 current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
606 );
607 if (next.has(fileKey)) {
608 next.delete(fileKey);
609 } else {
610 next.add(fileKey);
611 }
612 return { scopeKey: collapseScopeKey, fileKeys: next };
613 });
614 },
615 [collapseScopeKey, defaultCollapsedDiffFileKeys],
616 );
669 const collapseDefaultsRef = useRef({ collapseScopeKey, defaultCollapsedDiffFileKeys });
670 useLayoutEffect(() => {
671 collapseDefaultsRef.current = { collapseScopeKey, defaultCollapsedDiffFileKeys };
672 }, [collapseScopeKey, defaultCollapsedDiffFileKeys]);
673 const toggleDiffFileCollapsed = useCallback((fileKey: string) => {
674 const { collapseScopeKey, defaultCollapsedDiffFileKeys } = collapseDefaultsRef.current;
675 setCollapsedDiffFiles((current) => {
676 const next = new Set(
677 current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
678 );
679 if (next.has(fileKey)) {
680 next.delete(fileKey);
681 } else {
682 next.add(fileKey);
683 }
684 return { scopeKey: collapseScopeKey, fileKeys: next };
685 });
686 }, []);
617687
618688 const toggleDiffFileCollapse = useCallback(() => {
619689 setCodeViewRevision((current) => current + 1);
......@@ -1082,14 +1152,15 @@ export default function DiffPanel({
10821152 composerDraftTarget={composerDraftTarget}
10831153 renderHeaderFilenameSuffix={(fileDiff) => {
10841154 const path = resolveFileDiffPath(fileDiff);
1085 const stat = fileStats.get(path);
1155 const state = fileStates.get(path);
10861156 return (
1087 <>
1088 <DiffFilePathCopyButton filePath={path} />
1089 {stat ? (
1090 <DiffFileStatus {...fileStates.get(path)} retry={() => retry(path)} />
1091 ) : null}
1092 </>
1157 <DiffFileHeaderSuffix
1158 filePath={path}
1159 hasStat={fileStats.has(path)}
1160 error={state?.error ?? false}
1161 truncated={state?.truncated ?? false}
1162 onRetry={retry}
1163 />
10931164 );
10941165 }}
10951166 {...(lazySource
......@@ -1109,37 +1180,15 @@ export default function DiffPanel({
11091180 : {})}
11101181 renderHeaderPrefix={(fileDiff, fileKey) => {
11111182 const unavailable = fileDiff.cacheKey?.endsWith(":pending") === true;
1112 const collapsed = unavailable || collapsedDiffFileKeys.has(fileKey);
1113 const filePath = resolveFileDiffPath(fileDiff);
11141183 return (
1115 <Tooltip>
1116 <TooltipTrigger
1117 render={
1118 <Button
1119 size="icon-micro"
1120 variant="ghost"
1121 className="-ms-0.5"
1122 aria-label={
1123 collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`
1124 }
1125 aria-expanded={!collapsed}
1126 disabled={unavailable}
1127 onClick={(event) => {
1128 event.stopPropagation();
1129 toggleDiffFileCollapsed(fileKey);
1130 }}
1131 />
1132 }
1133 >
1134 <MorphIcon
1135 className={cn("size-4", getDiffCollapseIconClassName(fileDiff))}
1136 icon={collapsed ? ChevronRight : ChevronDown}
1137 />
1138 </TooltipTrigger>
1139 <TooltipPopup side="top">
1140 {collapsed ? "Expand diff" : "Collapse diff"}
1141 </TooltipPopup>
1142 </Tooltip>
1184 <DiffFileCollapseToggle
1185 filePath={resolveFileDiffPath(fileDiff)}
1186 fileKey={fileKey}
1187 collapsed={unavailable || collapsedDiffFileKeys.has(fileKey)}
1188 unavailable={unavailable}
1189 iconClassName={getDiffCollapseIconClassName(fileDiff)}
1190 onToggle={toggleDiffFileCollapsed}
1191 />
11431192 );
11441193 }}
11451194 options={{
apps/web/src/components/diffs/AnnotatableCodeView.tsx+6-6
......@@ -11,7 +11,7 @@ import type { ScopedThreadRef } from "@t3tools/contracts";
1111import { useCallback, useMemo, useState, type ReactNode, type Ref } from "react";
1212
1313import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
14import { fnv1a32 } from "~/lib/diffRendering";
14import { fnv1a32, resolveFileDiffPath } from "~/lib/diffRendering";
1515import {
1616 buildDiffReviewComment,
1717 restoreDiffReviewCommentRange,
......@@ -217,15 +217,15 @@ export function AnnotatableCodeView({
217217 if (!range) return;
218218 const item = context.item;
219219 if (item.type !== "diff") return;
220 const file = filesByKey.get(item.id);
221 if (!file) return;
220 // Read from the item, not the file list, so this callback keeps its identity as
221 // patches arrive; the viewer re-applies its options whenever it changes.
222222 const id = nextFileCommentId();
223223 const comment = buildDiffReviewComment({
224224 id,
225225 sectionId,
226226 sectionTitle,
227 filePath: file.filePath,
228 fileDiff: file.fileDiff,
227 filePath: resolveFileDiffPath(item.fileDiff),
228 fileDiff: item.fileDiff,
229229 range,
230230 text: "",
231231 });
......@@ -242,7 +242,7 @@ export function AnnotatableCodeView({
242242 },
243243 });
244244 },
245 [filesByKey, sectionId, sectionTitle],
245 [sectionId, sectionTitle],
246246 );
247247
248248 const hasOpenComment = draft !== null;
apps/web/src/components/diffs/useReviewFilePatches.ts+36-19
......@@ -3,7 +3,15 @@ import type { FileDiffMetadata } from "@pierre/diffs";
33import type { EnvironmentId, ReviewDiffPreviewSource } from "@t3tools/contracts";
44import * as AsyncResult from "effect/reactivity/AsyncResult";
55import * as Atom from "effect/reactivity/Atom";
6import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6import {
7 useCallback,
8 useContext,
9 useEffect,
10 useLayoutEffect,
11 useMemo,
12 useRef,
13 useState,
14} from "react";
715import { getRenderablePatch, resolveFileDiffPath, type RenderablePatch } from "~/lib/diffRendering";
816import { reviewEnvironment } from "~/state/review";
917
......@@ -146,12 +154,17 @@ export function useReviewFilePatches({
146154 [requestFiles, settledFileCount],
147155 );
148156 const requestFile = useCallback((index: number) => requestFiles([index]), [requestFiles]);
157 const retryInputsRef = useRef({ queries, files });
158 useLayoutEffect(() => {
159 retryInputsRef.current = { queries, files };
160 }, [queries, files]);
149161 const retry = useCallback(
150162 (path: string) => {
163 const { queries, files } = retryInputsRef.current;
151164 const query = queries.find(({ index }) => files[index]?.path === path)?.query;
152165 if (query) registry.refresh(query);
153166 },
154 [queries, files, registry],
167 [registry],
155168 );
156169 const renderableFiles = useMemo(
157170 () =>
......@@ -185,23 +198,27 @@ export function useReviewFilePatches({
185198 ),
186199 [source, files, patches, scope, preview],
187200 );
188 const fileStates = new Map(
189 files.map((file, index) => {
190 const patch = patches.get(index);
191 return [
192 file.path,
193 {
194 error:
195 patch?._tag === "Failure" ||
196 (patch?._tag === "Success" &&
197 (patch.value.patch?.kind !== "files" ||
198 !patch.value.patch.files.some(
199 (candidate) => resolveFileDiffPath(candidate) === file.path,
200 ))),
201 truncated: patch?._tag === "Success" && patch.value.source.truncated,
202 },
203 ] as const;
204 }),
201 const fileStates = useMemo(
202 () =>
203 new Map(
204 files.map((file, index) => {
205 const patch = patches.get(index);
206 return [
207 file.path,
208 {
209 error:
210 patch?._tag === "Failure" ||
211 (patch?._tag === "Success" &&
212 (patch.value.patch?.kind !== "files" ||
213 !patch.value.patch.files.some(
214 (candidate) => resolveFileDiffPath(candidate) === file.path,
215 ))),
216 truncated: patch?._tag === "Success" && patch.value.source.truncated,
217 },
218 ] as const;
219 }),
220 ),
221 [files, patches],
205222 );
206223 const readyFilePaths = useMemo(
207224 () =>