fix: optimize agent git diff rendering (#1564)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-17 13:19:53 -07:00 • committed by GitHub
parent 5e0fb95f2e
commit e4d737e18c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 349 additions and 182 deletions

View file

@ -1,5 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { MultiFileDiff } from "@pierre/diffs/react"
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
MultiFileDiff,
Virtualizer,
WorkerPoolContextProvider,
} from "@pierre/diffs/react"
import {
FileTree,
useFileTree,
@ -13,6 +17,12 @@ import {
GitPullRequestIcon,
SidebarSimpleIcon,
} from "@phosphor-icons/react"
import type {
FileContents,
VirtualFileMetrics,
WorkerInitializationRenderOptions,
WorkerPoolOptions,
} from "@pierre/diffs/react"
import type { GitStatus, GitStatusEntry } from "@pierre/trees"
import type { AgentThread, Message } from "@/lib/agents/types"
@ -83,6 +93,38 @@ const PANEL_MIN_WIDTH = 320
// Exported so the chat column can enforce the same floor via min-width.
export const PANEL_MIN_CHAT_WIDTH = 360
const DIFF_VIRTUALIZER_CONFIG = {
overscrollSize: 1200,
intersectionObserverMargin: 4800,
}
const DIFF_VIRTUAL_METRICS = {
hunkLineCount: 80,
lineHeight: 18,
diffHeaderHeight: 0,
spacing: 8,
} satisfies Partial<VirtualFileMetrics>
const DIFF_WORKER_POOL_OPTIONS = {
workerFactory: () =>
new Worker(
new URL("@pierre/diffs/worker/worker-portable.js", import.meta.url),
{
type: "module",
}
),
poolSize: 2,
totalASTLRUCacheSize: 120,
} satisfies WorkerPoolOptions
const DIFF_WORKER_HIGHLIGHTER_OPTIONS = {
theme: { light: "pierre-light", dark: "pierre-dark" },
lineDiffType: "word-alt",
maxLineDiffLength: 800,
tokenizeMaxLineLength: 1200,
langs: ["text"],
} satisfies WorkerInitializationRenderOptions
function getPanelMaxWidth(availableWidth?: number): number {
if (typeof window === "undefined") return PANEL_DEFAULT_WIDTH
const available = availableWidth ?? window.innerWidth
@ -90,7 +132,10 @@ function getPanelMaxWidth(availableWidth?: number): number {
}
function clampPanelWidth(width: number, availableWidth?: number): number {
return Math.min(getPanelMaxWidth(availableWidth), Math.max(PANEL_MIN_WIDTH, width))
return Math.min(
getPanelMaxWidth(availableWidth),
Math.max(PANEL_MIN_WIDTH, width)
)
}
function readStoredPanelWidth(): number {
@ -105,46 +150,84 @@ function readStoredPanelCollapsed(): boolean {
if (typeof window === "undefined") return true
// Default to collapsed until the user opens it once.
return (
window.localStorage.getItem(PANEL_STORAGE_COLLAPSED) !== COLLAPSED_STATE_FALSE
window.localStorage.getItem(PANEL_STORAGE_COLLAPSED) !==
COLLAPSED_STATE_FALSE
)
}
function PanelResizeHandle({
width,
onResize,
onResizeEnd,
}: {
width: number
onResize: (next: number) => void
onResize: (next: number) => number
onResizeEnd: (next: number) => void
}) {
const startRef = useRef<{ x: number; width: number } | null>(null)
const pendingWidthRef = useRef<number | null>(null)
const latestWidthRef = useRef(width)
const frameRef = useRef<number | null>(null)
const [dragging, setDragging] = useState(false)
useEffect(() => {
latestWidthRef.current = width
}, [width])
const flushResize = useCallback(() => {
frameRef.current = null
const next = pendingWidthRef.current
pendingWidthRef.current = null
if (next == null) return
latestWidthRef.current = onResize(next)
}, [onResize])
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault()
startRef.current = { x: e.clientX, width }
startRef.current = { x: e.clientX, width: latestWidthRef.current }
setDragging(true)
e.currentTarget.setPointerCapture(e.pointerId)
}
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (!startRef.current) return
onResize(startRef.current.width - (e.clientX - startRef.current.x))
pendingWidthRef.current =
startRef.current.width - (e.clientX - startRef.current.x)
if (frameRef.current == null) {
frameRef.current = window.requestAnimationFrame(flushResize)
}
}
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
if (frameRef.current != null) {
window.cancelAnimationFrame(frameRef.current)
flushResize()
}
startRef.current = null
setDragging(false)
onResizeEnd(latestWidthRef.current)
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId)
}
}
useEffect(() => {
if (!dragging) return
const prev = document.body.style.cursor
document.body.style.cursor = "col-resize"
return () => {
document.body.style.cursor = prev
if (frameRef.current != null) {
window.cancelAnimationFrame(frameRef.current)
}
}
}, [])
useEffect(() => {
if (!dragging) return
const prevCursor = document.body.style.cursor
const prevUserSelect = document.body.style.userSelect
document.body.style.cursor = "col-resize"
document.body.style.userSelect = "none"
return () => {
document.body.style.cursor = prevCursor
document.body.style.userSelect = prevUserSelect
}
}, [dragging])
@ -237,22 +320,38 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
}
}
const setWidth = useCallback((next: number) => {
const available = panelRef.current?.parentElement?.clientWidth
const clamped = clampPanelWidth(next, available)
setWidthState(clamped)
window.localStorage.setItem(PANEL_STORAGE_WIDTH, String(clamped))
}, [])
const applyWidth = useCallback(
(next: number) => {
const available = panelRef.current?.parentElement?.clientWidth
const clamped = clampPanelWidth(next, available)
if (!overlay && panelRef.current) {
panelRef.current.style.width = `${clamped}px`
}
return clamped
},
[overlay]
)
const commitWidth = useCallback(
(next: number) => {
const clamped = applyWidth(next)
setWidthState((current) => (current === clamped ? current : clamped))
if (typeof window !== "undefined") {
window.localStorage.setItem(PANEL_STORAGE_WIDTH, String(clamped))
}
},
[applyWidth]
)
// Re-clamp against the real container width on mount and whenever the window
// resizes, so the panel can never squeeze the chat below its minimum width.
useEffect(() => {
if (typeof window === "undefined") return
const reclamp = () => setWidth(width)
const reclamp = () => commitWidth(width)
reclamp()
window.addEventListener("resize", reclamp)
return () => window.removeEventListener("resize", reclamp)
}, [setWidth, width])
}, [commitWidth, width])
const [selectedTreePath, setSelectedTreePath] = useState<string | null>(null)
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
const pr = thread.pr
@ -406,171 +505,234 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
overlay ? "mx-3 mb-3" : "mr-4 mb-4 ml-1"
)}
>
{topTab !== "git" ? (
<div className="flex flex-1 items-center justify-center p-6 text-xs text-[var(--ui-text-dim)]">
Coming Soon
</div>
) : (
<>
{pr && (
<div className="border-b border-[var(--ui-border)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-[var(--ui-text)]">
{pr.title} #{pr.number}
</div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-[var(--ui-text-dim)]">
<span className="inline-flex items-center gap-1 rounded border border-[var(--ui-border)] px-1.5 py-0.5 capitalize">
<GitPullRequestIcon className="size-3" />
{pr.state}
</span>
<span>
{pr.headRef} → {pr.baseRef}
</span>
</div>
</div>
{pr.url && (
<a
href={pr.url}
target="_blank"
rel="noreferrer"
className={buttonVariants({ variant: "outline", size: "sm" })}
>
<ArrowSquareOutIcon className="size-3" />
View PR
</a>
)}
{topTab !== "git" ? (
<div className="flex flex-1 items-center justify-center p-6 text-xs text-[var(--ui-text-dim)]">
Coming Soon
</div>
</div>
)}
<div className="flex items-center gap-1 border-b border-[var(--ui-border)] px-3 py-2">
{(
[
["diff", "Diff"],
["review", "Review"],
["commits", "Commits"],
] as const
).map(([id, label]) => (
<button
key={id}
type="button"
onClick={() => setTab(id)}
className={cn(
"rounded-md px-2.5 py-1 text-xs transition-colors",
tab === id
? "bg-[var(--ui-accent-bubble)] font-medium text-[var(--ui-text)]"
: "text-[var(--ui-text-dim)] hover:bg-[var(--ui-panel-2)]"
) : (
<>
{pr && (
<div className="border-b border-[var(--ui-border)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-[var(--ui-text)]">
{pr.title} #{pr.number}
</div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-[var(--ui-text-dim)]">
<span className="inline-flex items-center gap-1 rounded border border-[var(--ui-border)] px-1.5 py-0.5 capitalize">
<GitPullRequestIcon className="size-3" />
{pr.state}
</span>
<span>
{pr.headRef} → {pr.baseRef}
</span>
</div>
</div>
{pr.url && (
<a
href={pr.url}
target="_blank"
rel="noreferrer"
className={buttonVariants({
variant: "outline",
size: "sm",
})}
>
<ArrowSquareOutIcon className="size-3" />
View PR
</a>
)}
</div>
</div>
)}
>
{label}
</button>
))}
{files.length > 0 && (
<span className="ml-auto flex items-center gap-2 text-[11px] text-[var(--ui-text-dim)]">
<span>
{files.length} file{files.length === 1 ? "" : "s"}
</span>
<span className="text-[var(--ui-success)]">
+{totals.additions}
</span>
<span className="text-[var(--ui-danger)]">-{totals.deletions}</span>
</span>
)}
</div>
<div className="flex min-h-0 flex-1">
<div className="min-h-0 flex-1 overflow-y-auto">
{tab === "diff" && files.length > 0 ? (
<div className="space-y-2 p-2">
{files.map((file) => (
<FileDiffSection
key={file.filePath}
file={file}
sectionRef={(node) => {
sectionRefs.current[file.filePath] = node
}}
/>
<div className="flex items-center gap-1 border-b border-[var(--ui-border)] px-3 py-2">
{(
[
["diff", "Diff"],
["review", "Review"],
["commits", "Commits"],
] as const
).map(([id, label]) => (
<button
key={id}
type="button"
onClick={() => setTab(id)}
className={cn(
"rounded-md px-2.5 py-1 text-xs transition-colors",
tab === id
? "bg-[var(--ui-accent-bubble)] font-medium text-[var(--ui-text)]"
: "text-[var(--ui-text-dim)] hover:bg-[var(--ui-panel-2)]"
)}
>
{label}
</button>
))}
{files.length > 0 && (
<span className="ml-auto flex items-center gap-2 text-[11px] text-[var(--ui-text-dim)]">
<span>
{files.length} file{files.length === 1 ? "" : "s"}
</span>
<span className="text-[var(--ui-success)]">
+{totals.additions}
</span>
<span className="text-[var(--ui-danger)]">
-{totals.deletions}
</span>
</span>
)}
</div>
) : (
<div className="p-6 text-center text-xs text-[var(--ui-text-dim)]">
{tab !== "diff"
? "Coming Soon"
: prDiff.isLoading
? "Loading PR diff…"
: "No diff available."}
</div>
)}
</div>
{fullScreen && !isMobile && files.length > 0 && (
<div className="w-72 shrink-0 border-l border-[var(--ui-border)] bg-[var(--ui-surface)]">
<FileTreeExplorer
files={files}
selectedTreePath={selectedTreePath}
onSelect={setSelectedTreePath}
/>
</div>
<div className="flex min-h-0 flex-1">
{tab === "diff" && files.length > 0 ? (
<WorkerPoolContextProvider
poolOptions={DIFF_WORKER_POOL_OPTIONS}
highlighterOptions={DIFF_WORKER_HIGHLIGHTER_OPTIONS}
>
<Virtualizer
className="min-h-0 flex-1 overflow-y-auto"
contentClassName="space-y-2 p-2"
config={DIFF_VIRTUALIZER_CONFIG}
>
{files.map((file) => (
<FileDiffSection
key={file.filePath}
file={file}
sectionRef={(node) => {
sectionRefs.current[file.filePath] = node
}}
/>
))}
</Virtualizer>
</WorkerPoolContextProvider>
) : (
<div className="min-h-0 flex-1 overflow-y-auto p-6 text-center text-xs text-[var(--ui-text-dim)]">
{tab !== "diff"
? "Coming Soon"
: prDiff.isLoading
? "Loading PR diff…"
: "No diff available."}
</div>
)}
{fullScreen && !isMobile && files.length > 0 && (
<div className="w-72 shrink-0 border-l border-[var(--ui-border)] bg-[var(--ui-surface)]">
<FileTreeExplorer
files={files}
selectedTreePath={selectedTreePath}
onSelect={setSelectedTreePath}
/>
</div>
)}
</div>
</>
)}
</div>
</>
{!overlay && (
<PanelResizeHandle
width={width}
onResize={applyWidth}
onResizeEnd={commitWidth}
/>
)}
</div>
{!overlay && <PanelResizeHandle width={width} onResize={setWidth} />}
</aside>
)
}
function FileDiffSection({
file,
sectionRef,
}: {
file: PanelFile
sectionRef: (node: HTMLDivElement | null) => void
}) {
const [open, setOpen] = useState(true)
const diffOptions = useDiffOptions()
return (
<div
ref={sectionRef}
className="mb-2 scroll-mt-2 overflow-hidden rounded-lg border border-[var(--ui-border)]"
>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 bg-[var(--ui-panel-2)] px-3 py-2 text-left text-xs"
>
<CaretDownIcon
className={cn("size-3 transition-transform", !open && "-rotate-90")}
/>
<span className="truncate font-medium text-[var(--ui-text)]">
{file.treePath}
</span>
<span className="ml-auto flex shrink-0 items-center gap-2">
<span className="text-[var(--ui-success)]">+{file.additions}</span>
<span className="text-[var(--ui-danger)]">-{file.deletions}</span>
</span>
</button>
{open &&
(file.unrenderable ? (
<div className="bg-[var(--ui-panel)] p-4 text-center text-xs text-[var(--ui-text-dim)]">
Binary or large file — diff not shown.
</div>
) : (
<div className="max-h-[420px] overflow-auto bg-[var(--ui-panel)] p-2 font-mono text-[11px] leading-5">
<MultiFileDiff
oldFile={{ name: file.treePath, contents: file.originalContent }}
newFile={{ name: file.treePath, contents: file.modifiedContent }}
options={diffOptions}
/>
</div>
))}
</div>
)
function hashFileContents(contents: string): string {
let hash = 0x811c9dc5
for (let i = 0; i < contents.length; i++) {
hash ^= contents.charCodeAt(i)
hash = Math.imul(hash, 0x01000193)
}
return (hash >>> 0).toString(36)
}
function fileContentsCacheKey(
path: string,
side: "old" | "new",
contents: string
): string {
return `${path}:${side}:${contents.length}:${hashFileContents(contents)}`
}
const FileDiffSection = memo(
function FileDiffSection({
file,
sectionRef,
}: {
file: PanelFile
sectionRef: (node: HTMLDivElement | null) => void
}) {
const [open, setOpen] = useState(true)
const diffOptions = useDiffOptions()
const oldFile = useMemo<FileContents>(
() => ({
name: file.treePath,
contents: file.originalContent,
cacheKey: fileContentsCacheKey(
file.filePath,
"old",
file.originalContent
),
}),
[file.filePath, file.originalContent, file.treePath]
)
const newFile = useMemo<FileContents>(
() => ({
name: file.treePath,
contents: file.modifiedContent,
cacheKey: fileContentsCacheKey(
file.filePath,
"new",
file.modifiedContent
),
}),
[file.filePath, file.modifiedContent, file.treePath]
)
return (
<div
ref={sectionRef}
className="mb-2 scroll-mt-2 overflow-hidden rounded-lg border border-[var(--ui-border)]"
>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 bg-[var(--ui-panel-2)] px-3 py-2 text-left text-xs"
>
<CaretDownIcon
className={cn("size-3 transition-transform", !open && "-rotate-90")}
/>
<span className="truncate font-medium text-[var(--ui-text)]">
{file.treePath}
</span>
<span className="ml-auto flex shrink-0 items-center gap-2">
<span className="text-[var(--ui-success)]">+{file.additions}</span>
<span className="text-[var(--ui-danger)]">-{file.deletions}</span>
</span>
</button>
{open &&
(file.unrenderable ? (
<div className="bg-[var(--ui-panel)] p-4 text-center text-xs text-[var(--ui-text-dim)]">
Binary or large file — diff not shown.
</div>
) : (
<div className="overflow-hidden bg-[var(--ui-panel)] p-2">
<MultiFileDiff
oldFile={oldFile}
newFile={newFile}
options={diffOptions}
metrics={DIFF_VIRTUAL_METRICS}
/>
</div>
))}
</div>
)
},
(prev, next) => prev.file === next.file
)
function FileTreeExplorer({
files,
selectedTreePath,

View file

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { preloadHighlighter } from "@pierre/diffs";
import { useResolvedTheme } from "@/lib/theme";
import { useMemo } from "react"
import { preloadHighlighter } from "@pierre/diffs"
import { useResolvedTheme } from "@/lib/theme"
export const DIFF_UNSAFE_CSS = `
[data-diffs-header],
@ -55,7 +55,7 @@ export const DIFF_UNSAFE_CSS = `
background-color: var(--ui-accent-bubble) !important;
color: var(--ui-text-dim) !important;
}
`;
`
export const diffOptions = {
theme: { light: "pierre-light", dark: "pierre-dark" } as const,
@ -65,17 +65,21 @@ export const diffOptions = {
disableFileHeader: true,
unsafeCSS: DIFF_UNSAFE_CSS,
collapsedContextThreshold: 4,
};
lineDiffType: "word-alt" as const,
maxLineDiffLength: 800,
tokenizeMaxLineLength: 1200,
tokenizeMaxLength: 120_000,
}
export function useDiffOptions() {
const resolvedTheme = useResolvedTheme();
const resolvedTheme = useResolvedTheme()
return useMemo(
() => ({ ...diffOptions, themeType: resolvedTheme }),
[resolvedTheme]
);
)
}
let highlighterWarmup: Promise<void> | null = null;
let highlighterWarmup: Promise<void> | null = null
/**
* Pierre's <MultiFileDiff> renders an empty <diffs-container> on its first mount
@ -88,15 +92,15 @@ let highlighterWarmup: Promise<void> | null = null;
* Idempotent and client-only (preloadHighlighter creates a Shiki instance).
*/
export function warmDiffHighlighter(): Promise<void> {
if (typeof window === "undefined") return Promise.resolve();
if (typeof window === "undefined") return Promise.resolve()
if (highlighterWarmup == null) {
highlighterWarmup = preloadHighlighter({
themes: [diffOptions.theme.light, diffOptions.theme.dark],
langs: ["text"],
}).catch((error) => {
highlighterWarmup = null;
throw error;
});
highlighterWarmup = null
throw error
})
}
return highlighterWarmup;
return highlighterWarmup
}

View file

@ -9,6 +9,7 @@ import { VitePWA } from "vite-plugin-pwa"
const config = defineConfig({
optimizeDeps: { include: ["workbox-window"] },
worker: { format: "es" },
plugins: [
devtools(),
nitro(),