authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-18 17:56:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-18 21:59:17-08:00
logbd1c7e1d9ae77e1c047e19ef895c089c00d87aa5
tree9768a87765493d76c1e1299c7344e30dbf63066e
parent453fb4992e9333be607322af1c2c2870ad93135d

Virtualize DiffPanel rendering with @pierre/diffs beta

- upgrade `@pierre/diffs` to `^1.1.0-beta.16` - switch file diff list to `Virtualizer` for smoother large patch rendering - adjust diff panel container/layout handling and theme typing

4 files changed, 61 insertions(+), 37 deletions(-)

apps/web/package.json+1-1
...@@ -12,7 +12,7 @@...@@ -12,7 +12,7 @@
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@base-ui/react": "^1.2.0",14 "@base-ui/react": "^1.2.0",
15 "@pierre/diffs": "^1.0.11",15 "@pierre/diffs": "^1.1.0-beta.16",
16 "@t3tools/contracts": "workspace:*",16 "@t3tools/contracts": "workspace:*",
17 "@tanstack/react-pacer": "^0.19.4",17 "@tanstack/react-pacer": "^0.19.4",
18 "@tanstack/react-query": "^5.90.0",18 "@tanstack/react-query": "^5.90.0",
apps/web/src/App.tsx+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";1import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
2import { Activity, Suspense, lazy, useEffect, useRef, useState } from "react";2import { Activity, Suspense, lazy, useEffect, useRef } from "react";
33
4import ChatView from "./components/ChatView";4import ChatView from "./components/ChatView";
5import Sidebar from "./components/Sidebar";5import Sidebar from "./components/Sidebar";
apps/web/src/components/DiffPanel.tsx+57-33
...@@ -1,5 +1,10 @@...@@ -1,5 +1,10 @@
1import { parsePatchFiles } from "@pierre/diffs";1import { parsePatchFiles } from "@pierre/diffs";
2import { FileDiff, type FileDiffMetadata, WorkerPoolContextProvider } from "@pierre/diffs/react";2import {
3 FileDiff,
4 type FileDiffMetadata,
5 Virtualizer,
6 WorkerPoolContextProvider,
7} from "@pierre/diffs/react";
3import { useQuery, useQueryClient } from "@tanstack/react-query";8import { useQuery, useQueryClient } from "@tanstack/react-query";
4import { Columns2Icon, Rows3Icon } from "lucide-react";9import { Columns2Icon, Rows3Icon } from "lucide-react";
5import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";10import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
...@@ -15,6 +20,7 @@ import { useStore } from "../store";...@@ -15,6 +20,7 @@ import { useStore } from "../store";
15import { ToggleGroup, Toggle } from "./ui/toggle-group";20import { ToggleGroup, Toggle } from "./ui/toggle-group";
1621
17type DiffRenderMode = "stacked" | "split";22type DiffRenderMode = "stacked" | "split";
23type DiffThemeType = "light" | "dark" | "system";
1824
19type RenderablePatch =25type RenderablePatch =
20 | {26 | {
...@@ -61,6 +67,10 @@ function resolveFileDiffPath(fileDiff: FileDiffMetadata): string {...@@ -61,6 +67,10 @@ function resolveFileDiffPath(fileDiff: FileDiffMetadata): string {
61 return raw;67 return raw;
62}68}
6369
70function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
71 return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`;
72}
73
64interface DiffPanelProps {74interface DiffPanelProps {
65 mode?: "inline" | "sheet";75 mode?: "inline" | "sheet";
66}76}
...@@ -396,46 +406,60 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -396,46 +406,60 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
396 </div>406 </div>
397 ) : (407 ) : (
398 <>408 <>
399 <div ref={patchViewportRef} className="min-w-0 flex-1 overflow-auto px-3 py-2">409 <div ref={patchViewportRef} className="min-h-0 min-w-0 flex-1 overflow-hidden">
400 {!canApplyStoredTarget && state.diffThreadId && (410 {!canApplyStoredTarget && state.diffThreadId && (
401 <p className="mb-2 text-[11px] text-muted-foreground/65">411 <div className="px-3 pt-2">
402 Showing diffs for the active thread.412 <p className="mb-2 text-[11px] text-muted-foreground/65">
403 </p>413 Showing diffs for the active thread.
414 </p>
415 </div>
404 )}416 )}
405 {checkpointDiffError && !renderablePatch && (417 {checkpointDiffError && !renderablePatch && (
406 <p className="mb-2 text-[11px] text-red-500/80">{checkpointDiffError}</p>418 <div className="px-3">
419 <p className="mb-2 text-[11px] text-red-500/80">{checkpointDiffError}</p>
420 </div>
407 )}421 )}
408 {!renderablePatch ? (422 {!renderablePatch ? (
409 <div className="flex h-full items-center justify-center text-xs text-muted-foreground/70">423 <div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
410 {isLoadingCheckpointDiff424 <p>
411 ? "Loading checkpoint diff..."425 {isLoadingCheckpointDiff
412 : "No patch available for this selection."}426 ? "Loading checkpoint diff..."
427 : "No patch available for this selection."}
428 </p>
413 </div>429 </div>
414 ) : renderablePatch.kind === "files" ? (430 ) : renderablePatch.kind === "files" ? (
415 <div className="space-y-3">431 <Virtualizer
416 {renderableFiles.map((fileDiff) => (432 className="h-full min-h-0 overflow-auto px-3 py-2"
417 <div433 config={{
418 key={fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`}434 overscrollSize: 600,
419 data-diff-file-path={resolveFileDiffPath(fileDiff)}435 intersectionObserverMargin: 1200,
420 className="rounded-md"436 }}
421 >437 >
422 <FileDiff438 {renderableFiles.map((fileDiff) => {
423 fileDiff={fileDiff}439 const filePath = resolveFileDiffPath(fileDiff);
424 options={{440 const fileKey = buildFileDiffRenderKey(fileDiff);
425 diffStyle: diffRenderMode === "split" ? "split" : "unified",441 return (
426 lineDiffType: "none",442 <div key={fileKey} data-diff-file-path={filePath} className="rounded-md">
427 themeType: resolvedTheme,443 <FileDiff
428 }}444 fileDiff={fileDiff}
429 />445 options={{
430 </div>446 diffStyle: diffRenderMode === "split" ? "split" : "unified",
431 ))}447 lineDiffType: "none",
432 </div>448 themeType: resolvedTheme as DiffThemeType,
449 }}
450 />
451 </div>
452 );
453 })}
454 </Virtualizer>
433 ) : (455 ) : (
434 <div className="space-y-2">456 <div className="h-full overflow-auto px-3 py-2">
435 <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p>457 <div className="space-y-2">
436 <pre className="max-h-[72vh] overflow-auto rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90">458 <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p>
437 {renderablePatch.text}459 <pre className="max-h-[72vh] overflow-auto rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90">
438 </pre>460 {renderablePatch.text}
461 </pre>
462 </div>
439 </div>463 </div>
440 )}464 )}
441 </div>465 </div>
bun.lock+2-2
...@@ -52,7 +52,7 @@...@@ -52,7 +52,7 @@
52 "version": "0.0.0",52 "version": "0.0.0",
53 "dependencies": {53 "dependencies": {
54 "@base-ui/react": "^1.2.0",54 "@base-ui/react": "^1.2.0",
55 "@pierre/diffs": "^1.0.11",55 "@pierre/diffs": "^1.1.0-beta.16",
56 "@t3tools/contracts": "workspace:*",56 "@t3tools/contracts": "workspace:*",
57 "@tanstack/react-pacer": "^0.19.4",57 "@tanstack/react-pacer": "^0.19.4",
58 "@tanstack/react-query": "^5.90.0",58 "@tanstack/react-query": "^5.90.0",
...@@ -305,7 +305,7 @@...@@ -305,7 +305,7 @@
305305
306 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="],306 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.46.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DJbQsSJUr4KSi9uU0QqOgI7PX2C+fKGZX+YDprt3vM2sC0dWZsgVTLoN2vtkNyEWJSY2mnvRFUshWXT3bmo0Ug=="],
307307
308 "@pierre/diffs": ["@pierre/diffs@1.0.11", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw=="],308 "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="],
309309
310 "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],310 "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
311311