From 25d9f15c03da4d13bcb39b71040596c46f45db06 Mon Sep 17 00:00:00 2001 From: huxcrux Date: Sun, 15 Mar 2026 22:48:30 +0100 Subject: [PATCH 1/4] Add per-file diff collapse toggle --- apps/web/src/components/DiffPanel.tsx | 86 ++++++++++++++++++---- apps/web/src/lib/diffPanelCollapse.test.ts | 61 +++++++++++++++ apps/web/src/lib/diffPanelCollapse.ts | 52 +++++++++++++ 3 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/lib/diffPanelCollapse.test.ts create mode 100644 apps/web/src/lib/diffPanelCollapse.ts diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 96e0219872..dfa8169ec3 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -4,6 +4,8 @@ import { useQuery } from "@tanstack/react-query"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { ThreadId, type TurnId } from "@t3tools/contracts"; import { + ChevronDownIcon, + ChevronUpIcon, ChevronLeftIcon, ChevronRightIcon, Columns2Icon, @@ -26,6 +28,13 @@ import { readNativeApi } from "../nativeApi"; import { resolvePathLinkTarget } from "../terminal-links"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { useTheme } from "../hooks/useTheme"; +import { + buildFileDiffRenderKey, + expandCollapsedDiffFileForPath, + resetCollapsedDiffFiles, + resolveFileDiffPath, + toggleCollapsedDiffFile, +} from "../lib/diffPanelCollapse"; import { buildPatchCacheKey } from "../lib/diffRendering"; import { resolveDiffThemeName } from "../lib/diffRendering"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; @@ -100,6 +109,11 @@ const DIFF_PANEL_UNSAFE_CSS = ` color: color-mix(in srgb, var(--foreground) 84%, var(--primary)) !important; text-decoration-color: currentColor; } + +:host(.diff-file-collapsed) pre, +:host(.diff-file-collapsed) [data-virtualizer-buffer] { + display: none !important; +} `; type RenderablePatch = @@ -145,18 +159,6 @@ function getRenderablePatch( } } -function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { - const raw = fileDiff.name ?? fileDiff.prevName ?? ""; - if (raw.startsWith("a/") || raw.startsWith("b/")) { - return raw.slice(2); - } - return raw; -} - -function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { - return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; -} - interface DiffPanelProps { mode?: DiffPanelMode; } @@ -169,6 +171,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { const { settings } = useAppSettings(); const [diffRenderMode, setDiffRenderMode] = useState("stacked"); const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); + const [collapsedFileKeys, setCollapsedFileKeys] = + useState>(resetCollapsedDiffFiles); const patchViewportRef = useRef(null); const turnStripRef = useRef(null); const previousDiffOpenRef = useRef(false); @@ -309,15 +313,41 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { previousDiffOpenRef.current = diffOpen; }, [diffOpen, settings.diffWordWrap]); + useEffect(() => { + setCollapsedFileKeys(resetCollapsedDiffFiles()); + }, [selectedPatch]); + + useEffect(() => { + if (!selectedFilePath) { + return; + } + + setCollapsedFileKeys((current) => + expandCollapsedDiffFileForPath(current, renderableFiles, selectedFilePath), + ); + }, [renderableFiles, selectedFilePath]); + useEffect(() => { if (!selectedFilePath || !patchViewportRef.current) { return; } + + const selectedFile = renderableFiles.find( + (fileDiff) => resolveFileDiffPath(fileDiff) === selectedFilePath, + ); + if (!selectedFile) { + return; + } + + if (collapsedFileKeys.has(buildFileDiffRenderKey(selectedFile))) { + return; + } + const target = Array.from( patchViewportRef.current.querySelectorAll("[data-diff-file-path]"), ).find((element) => element.dataset.diffFilePath === selectedFilePath); target?.scrollIntoView({ block: "nearest" }); - }, [selectedFilePath, renderableFiles]); + }, [collapsedFileKeys, renderableFiles, selectedFilePath]); const openDiffFileInEditor = useCallback( (filePath: string) => { @@ -591,6 +621,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { const filePath = resolveFileDiffPath(fileDiff); const fileKey = buildFileDiffRenderKey(fileDiff); const themedFileKey = `${fileKey}:${resolvedTheme}`; + const isCollapsed = collapsedFileKeys.has(fileKey); return (
{ + if (!(node instanceof Element)) return false; + return node.hasAttribute("data-diff-collapse-toggle"); + }); + if (!clickedHeader || clickedCollapseToggle) return; openDiffFileInEditor(filePath); }} > ( + + )} />
); diff --git a/apps/web/src/lib/diffPanelCollapse.test.ts b/apps/web/src/lib/diffPanelCollapse.test.ts new file mode 100644 index 0000000000..8c3878dbcf --- /dev/null +++ b/apps/web/src/lib/diffPanelCollapse.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; + +import { + buildFileDiffRenderKey, + expandCollapsedDiffFileForPath, + resetCollapsedDiffFiles, + toggleCollapsedDiffFile, +} from "./diffPanelCollapse"; + +describe("diffPanelCollapse", () => { + const firstFile = { + name: "src/app.ts", + cacheKey: "file-1", + }; + const secondFile = { + name: "src/routes.ts", + cacheKey: "file-2", + }; + + it("defaults files to expanded", () => { + const collapsed = resetCollapsedDiffFiles(); + + expect(collapsed.has(buildFileDiffRenderKey(firstFile))).toBe(false); + }); + + it("toggles one file without affecting others", () => { + const collapsedFirst = toggleCollapsedDiffFile( + resetCollapsedDiffFiles(), + buildFileDiffRenderKey(firstFile), + ); + + expect(collapsedFirst.has(buildFileDiffRenderKey(firstFile))).toBe(true); + expect(collapsedFirst.has(buildFileDiffRenderKey(secondFile))).toBe(false); + }); + + it("resets all collapsed files for a new patch selection", () => { + const collapsed = toggleCollapsedDiffFile( + resetCollapsedDiffFiles(), + buildFileDiffRenderKey(firstFile), + ); + + expect(collapsed.size).toBe(1); + expect(resetCollapsedDiffFiles().size).toBe(0); + }); + + it("auto-expands the selected file path when it was collapsed", () => { + const renamedFile = { + name: "b/src/new-name.ts", + prevName: "a/src/old-name.ts", + cacheKey: "file-rename", + }; + const collapsed = toggleCollapsedDiffFile( + resetCollapsedDiffFiles(), + buildFileDiffRenderKey(renamedFile), + ); + + const expanded = expandCollapsedDiffFileForPath(collapsed, [renamedFile], "src/new-name.ts"); + + expect(expanded.has(buildFileDiffRenderKey(renamedFile))).toBe(false); + }); +}); diff --git a/apps/web/src/lib/diffPanelCollapse.ts b/apps/web/src/lib/diffPanelCollapse.ts new file mode 100644 index 0000000000..3ca99db069 --- /dev/null +++ b/apps/web/src/lib/diffPanelCollapse.ts @@ -0,0 +1,52 @@ +import type { FileDiffMetadata } from "@pierre/diffs/react"; + +export function resolveFileDiffPath(fileDiff: Pick): string { + const raw = fileDiff.name ?? fileDiff.prevName ?? ""; + if (raw.startsWith("a/") || raw.startsWith("b/")) { + return raw.slice(2); + } + return raw; +} + +export function buildFileDiffRenderKey( + fileDiff: Pick, +): string { + return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; +} + +export function toggleCollapsedDiffFile( + current: ReadonlySet, + fileKey: string, +): ReadonlySet { + const next = new Set(current); + if (next.has(fileKey)) { + next.delete(fileKey); + } else { + next.add(fileKey); + } + return next; +} + +export function resetCollapsedDiffFiles(): ReadonlySet { + return new Set(); +} + +export function expandCollapsedDiffFileForPath( + current: ReadonlySet, + files: ReadonlyArray>, + filePath: string, +): ReadonlySet { + const match = files.find((fileDiff) => resolveFileDiffPath(fileDiff) === filePath); + if (!match) { + return current; + } + + const fileKey = buildFileDiffRenderKey(match); + if (!current.has(fileKey)) { + return current; + } + + const next = new Set(current); + next.delete(fileKey); + return next; +} From 6edaefa5fe9de6e19aece6a4025f5232112f872b Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 24 Mar 2026 22:47:06 +0100 Subject: [PATCH 2/4] Fix diff panel scroll on collapse toggle --- apps/web/src/components/DiffPanel.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index dfa8169ec3..75936c91f8 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -176,6 +176,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { const patchViewportRef = useRef(null); const turnStripRef = useRef(null); const previousDiffOpenRef = useRef(false); + const pendingSelectedFileScrollRef = useRef(false); const [canScrollTurnStripLeft, setCanScrollTurnStripLeft] = useState(false); const [canScrollTurnStripRight, setCanScrollTurnStripRight] = useState(false); const routeThreadId = useParams({ @@ -317,6 +318,10 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { setCollapsedFileKeys(resetCollapsedDiffFiles()); }, [selectedPatch]); + useEffect(() => { + pendingSelectedFileScrollRef.current = Boolean(selectedFilePath); + }, [renderableFiles, selectedFilePath]); + useEffect(() => { if (!selectedFilePath) { return; @@ -329,6 +334,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { useEffect(() => { if (!selectedFilePath || !patchViewportRef.current) { + pendingSelectedFileScrollRef.current = false; return; } @@ -336,6 +342,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { (fileDiff) => resolveFileDiffPath(fileDiff) === selectedFilePath, ); if (!selectedFile) { + pendingSelectedFileScrollRef.current = false; return; } @@ -343,10 +350,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { return; } + if (!pendingSelectedFileScrollRef.current) { + return; + } + const target = Array.from( patchViewportRef.current.querySelectorAll("[data-diff-file-path]"), ).find((element) => element.dataset.diffFilePath === selectedFilePath); - target?.scrollIntoView({ block: "nearest" }); + if (!target) { + return; + } + + target.scrollIntoView({ block: "nearest" }); + pendingSelectedFileScrollRef.current = false; }, [collapsedFileKeys, renderableFiles, selectedFilePath]); const openDiffFileInEditor = useCallback( From 13f881c9f0e5319234b5fe6c97d91814329e8e9e Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 24 Mar 2026 22:50:42 +0100 Subject: [PATCH 3/4] Fix diff panel collapse toggle icon --- apps/web/src/components/DiffPanel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 75936c91f8..0c8561a459 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -5,7 +5,6 @@ import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { ThreadId, type TurnId } from "@t3tools/contracts"; import { ChevronDownIcon, - ChevronUpIcon, ChevronLeftIcon, ChevronRightIcon, Columns2Icon, @@ -685,7 +684,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { }} > {isCollapsed ? ( - + ) : ( )} From 4948c41003fa28ced629f739135954080f1bd5fe Mon Sep 17 00:00:00 2001 From: huxcrux Date: Tue, 24 Mar 2026 23:04:49 +0100 Subject: [PATCH 4/4] chore: retrigger ci