mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
fix: optimize agent git diff rendering (#1564)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
5e0fb95f2e
commit
e4d737e18c
3 changed files with 349 additions and 182 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { VitePWA } from "vite-plugin-pwa"
|
|||
|
||||
const config = defineConfig({
|
||||
optimizeDeps: { include: ["workbox-window"] },
|
||||
worker: { format: "es" },
|
||||
plugins: [
|
||||
devtools(),
|
||||
nitro(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue