From 1064bb2e317f2cb6d5ab4665dd3ba46ff8a4c69c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 29 Apr 2026 11:59:09 -0700 Subject: [PATCH 1/2] feat(web): collapsible file diffs in commit diff panel Hovering the status badge reveals a chevron; clicking it toggles the diff body. Collapsed state is held in the FileDiffList so it survives virtualization unmounts, and on collapse we scroll the row's header to the top of the viewport when it's already been scrolled past. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../commitDiffPanel/fileDiffList.tsx | 43 +++++++++++++++++-- .../commitDiffPanel/fileDiffRow.tsx | 28 ++++++------ .../components/commitDiffPanel/fileStatus.tsx | 40 ++++++++++++++--- 3 files changed, 88 insertions(+), 23 deletions(-) diff --git a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx index e05431692..dc243b63b 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx @@ -2,7 +2,7 @@ import { FileDiff } from "@/features/git"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { useMemo, useRef } from "react"; +import { useCallback, useRef, useState } from "react"; import { FileDiffRow } from "./fileDiffRow"; interface FileDiffListProps { @@ -23,7 +23,15 @@ const LINE_HEIGHT_PX = 18; const MIN_ROW_HEIGHT_PX = 200; const CONTEXT_LINES_PER_HUNK = 6; -const estimateRowHeight = (file: FileDiff): number => { +const getRowKey = (file: FileDiff, index: number): string => { + return file.newPath ?? file.oldPath ?? `idx-${index}`; +}; + +const estimateRowHeight = (file: FileDiff, isCollapsed: boolean): number => { + if (isCollapsed) { + return ROW_HEADER_PX; + } + const visibleLines = file.hunks.reduce((sum, hunk) => { return sum + Math.max(hunk.oldRange.lines, hunk.newRange.lines) + CONTEXT_LINES_PER_HUNK; }, 0); @@ -34,14 +42,39 @@ const estimateRowHeight = (file: FileDiff): number => { export const FileDiffList = ({ files, repoName, commitSha, parentSha }: FileDiffListProps) => { const parentRef = useRef(null); + const [collapsedKeys, setCollapsedKeys] = useState>(() => new Set()); + const collapsedKeysRef = useRef(collapsedKeys); + collapsedKeysRef.current = collapsedKeys; const virtualizer = useVirtualizer({ count: files.length, getScrollElement: () => parentRef.current, - estimateSize: (index) => estimateRowHeight(files[index]), + estimateSize: (index) => { + const file = files[index]; + return estimateRowHeight(file, collapsedKeys.has(getRowKey(file, index))); + }, overscan: 6, }); + const toggleCollapsed = useCallback((key: string, index: number, rowStart: number) => { + const willCollapse = !collapsedKeysRef.current.has(key); + setCollapsedKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + if (willCollapse) { + const scrollTop = parentRef.current?.scrollTop ?? 0; + if (scrollTop > rowStart) { + virtualizer.scrollToIndex(index, { align: 'start' }); + } + } + }, [virtualizer]); + return (
{virtualizer.getVirtualItems().map((virtualRow) => { const file = files[virtualRow.index]; - const rowKey = file.newPath ?? file.oldPath ?? `idx-${virtualRow.index}`; + const rowKey = getRowKey(file, virtualRow.index); return (
toggleCollapsed(rowKey, virtualRow.index, virtualRow.start)} />
); diff --git a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx index d7c5c50ee..b6a8c8c69 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx @@ -24,9 +24,11 @@ interface FileDiffRowProps { commitSha: string; // Null for the initial commit (no parent). parentSha: string | null; + isCollapsed: boolean; + onToggleCollapsed: () => void; } -export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: FileDiffRowProps) => { +export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha, isCollapsed, onToggleCollapsed }: FileDiffRowProps) => { const status = getFileStatus(file); // Deleted files don't exist at the commit, so the link points to the @@ -61,7 +63,7 @@ export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: F className="flex flex-row items-center gap-2 py-2 px-3 border-b bg-muted sticky z-10" style={{ top: `-${yOffset}px` }} > - +
{getDisplayPath(file)} @@ -75,16 +77,18 @@ export const FileDiffRow = ({ file, yOffset, repoName, commitSha, parentSha }: F /> )}
- {file.hunks.length === 0 ? ( -
- No textual diff (binary file or empty change). -
- ) : ( - + {!isCollapsed && ( + file.hunks.length === 0 ? ( +
+ No textual diff (binary file or empty change). +
+ ) : ( + + ) )}
); diff --git a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileStatus.tsx b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileStatus.tsx index 4e54b19f7..dcd717b57 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileStatus.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileStatus.tsx @@ -1,4 +1,5 @@ import type { FileDiff } from "@/features/git"; +import { ChevronDown, ChevronRight } from "lucide-react"; export type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed'; @@ -29,10 +30,35 @@ const STATUS_BADGE_COLORS: Record = { renamed: 'bg-blue-500/20 text-blue-700 dark:text-blue-400', }; -export const StatusBadge = ({ status }: { status: FileStatus }) => ( - - {STATUS_BADGE_LABELS[status]} - -); +interface StatusBadgeProps { + status: FileStatus; + onToggle?: () => void; + isCollapsed?: boolean; +} + +export const StatusBadge = ({ status, onToggle, isCollapsed }: StatusBadgeProps) => { + const baseClassName = `inline-flex items-center justify-center w-5 h-5 rounded text-xs font-mono font-bold ${STATUS_BADGE_COLORS[status]}`; + + if (!onToggle) { + return ( + + {STATUS_BADGE_LABELS[status]} + + ); + } + + const Chevron = isCollapsed ? ChevronRight : ChevronDown; + + return ( + + ); +}; From b936d9e273a309ee86de6a8d8713c8c40187ef4e Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 29 Apr 2026 11:59:39 -0700 Subject: [PATCH 2/2] docs: changelog entry for collapsible file diffs Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c96ee1b42..d3887f16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added commit diff viewer to code browser. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154) - Added `/api/commits/authors` to the public API to allow fetching a list of authors for a given path and revision. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150) - Added optional `path` query parameter to the `/api/diff` endpoint and `get_diff` MCP tool to restrict diffs to changes touching a specific file. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154) +- Added collapsible file diffs in the commit diff panel. [#1157](https://github.com/sourcebot-dev/sourcebot/pull/1157) ### Fixed - Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)