authorgravatar for crazywolf132@gmail.com0x1f99d <crazywolf132@gmail.com> 2026-03-13 14:47:29+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-12 20:47:29-07:00
logac0d1f52a087c63819b2b032ac4535d18d3c23af
tree3d4e795d6eb01d3ff96d5093e21d0e61b3f8e8a1
parentd0ae0adc9923e51f7027ee16afedb14b3ae7779c
signature Signed by PGP key B5690EEEBB952194

fix(web): defer diff worker startup until diff opens (#934)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

4 files changed, 148 insertions(+), 55 deletions(-)

apps/web/src/components/DiffPanel.tsx+13-31
...@@ -19,7 +19,6 @@ import { cn } from "~/lib/utils";...@@ -19,7 +19,6 @@ import { cn } from "~/lib/utils";
19import { readNativeApi } from "../nativeApi";19import { readNativeApi } from "../nativeApi";
20import { resolvePathLinkTarget } from "../terminal-links";20import { resolvePathLinkTarget } from "../terminal-links";
21import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";21import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
22import { isElectron } from "../env";
23import { useTheme } from "../hooks/useTheme";22import { useTheme } from "../hooks/useTheme";
24import { buildPatchCacheKey } from "../lib/diffRendering";23import { buildPatchCacheKey } from "../lib/diffRendering";
25import { resolveDiffThemeName } from "../lib/diffRendering";24import { resolveDiffThemeName } from "../lib/diffRendering";
...@@ -27,6 +26,7 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";...@@ -27,6 +26,7 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
27import { useStore } from "../store";26import { useStore } from "../store";
28import { useAppSettings } from "../appSettings";27import { useAppSettings } from "../appSettings";
29import { formatShortTimestamp } from "../timestampFormat";28import { formatShortTimestamp } from "../timestampFormat";
29import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
30import { ToggleGroup, Toggle } from "./ui/toggle-group";30import { ToggleGroup, Toggle } from "./ui/toggle-group";
3131
32type DiffRenderMode = "stacked" | "split";32type DiffRenderMode = "stacked" | "split";
...@@ -152,7 +152,7 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {...@@ -152,7 +152,7 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
152}152}
153153
154interface DiffPanelProps {154interface DiffPanelProps {
155 mode?: "inline" | "sheet" | "sidebar";155 mode?: DiffPanelMode;
156}156}
157157
158export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";158export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
...@@ -398,7 +398,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -398,7 +398,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
398 selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });398 selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
399 }, [selectedTurn?.turnId, selectedTurnId]);399 }, [selectedTurn?.turnId, selectedTurnId]);
400400
401 const shouldUseDragRegion = isElectron && mode !== "sheet";
402 const headerRow = (401 const headerRow = (
403 <>402 <>
404 <div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]">403 <div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]">
...@@ -512,28 +511,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -512,28 +511,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
512 </ToggleGroup>511 </ToggleGroup>
513 </>512 </>
514 );513 );
515 const headerRowClassName = cn(
516 "flex items-center justify-between gap-2 px-4",
517 shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
518 );
519514
520 return (515 return (
521 <div516 <DiffPanelShell mode={mode} header={headerRow}>
522 className={cn(
523 "flex h-full min-w-0 flex-col bg-background",
524 mode === "inline"
525 ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
526 : "w-full",
527 )}
528 >
529 {shouldUseDragRegion ? (
530 <div className={headerRowClassName}>{headerRow}</div>
531 ) : (
532 <div className="border-b border-border">
533 <div className={headerRowClassName}>{headerRow}</div>
534 </div>
535 )}
536
537 {!activeThread ? (517 {!activeThread ? (
538 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">518 <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
539 Select a thread to inspect turn diffs.519 Select a thread to inspect turn diffs.
...@@ -558,15 +538,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -558,15 +538,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
558 </div>538 </div>
559 )}539 )}
560 {!renderablePatch ? (540 {!renderablePatch ? (
561 <div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">541 isLoadingCheckpointDiff ? (
562 <p>542 <DiffPanelLoadingState label="Loading checkpoint diff..." />
563 {isLoadingCheckpointDiff543 ) : (
564 ? "Loading checkpoint diff..."544 <div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
565 : hasNoNetChanges545 <p>
546 {hasNoNetChanges
566 ? "No net changes in this selection."547 ? "No net changes in this selection."
567 : "No patch available for this selection."}548 : "No patch available for this selection."}
568 </p>549 </p>
569 </div>550 </div>
551 )
570 ) : renderablePatch.kind === "files" ? (552 ) : renderablePatch.kind === "files" ? (
571 <Virtualizer553 <Virtualizer
572 className="diff-render-surface h-full min-h-0 overflow-auto px-2 pb-2"554 className="diff-render-surface h-full min-h-0 overflow-auto px-2 pb-2"
...@@ -622,6 +604,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -622,6 +604,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
622 </div>604 </div>
623 </>605 </>
624 )}606 )}
625 </div>607 </DiffPanelShell>
626 );608 );
627}609}
apps/web/src/components/DiffPanelShell.tsx created+92
...@@ -0,0 +1,92 @@
1import type { ReactNode } from "react";
2
3import { isElectron } from "~/env";
4import { cn } from "~/lib/utils";
5
6import { Skeleton } from "./ui/skeleton";
7
8export type DiffPanelMode = "inline" | "sheet" | "sidebar";
9
10function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
11 const shouldUseDragRegion = isElectron && mode !== "sheet";
12 return cn(
13 "flex items-center justify-between gap-2 px-4",
14 shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
15 );
16}
17
18export function DiffPanelShell(props: {
19 mode: DiffPanelMode;
20 header: ReactNode;
21 children: ReactNode;
22}) {
23 const shouldUseDragRegion = isElectron && props.mode !== "sheet";
24
25 return (
26 <div
27 className={cn(
28 "flex h-full min-w-0 flex-col bg-background",
29 props.mode === "inline"
30 ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
31 : "w-full",
32 )}
33 >
34 {shouldUseDragRegion ? (
35 <div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
36 ) : (
37 <div className="border-b border-border">
38 <div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
39 </div>
40 )}
41 {props.children}
42 </div>
43 );
44}
45
46export function DiffPanelHeaderSkeleton() {
47 return (
48 <>
49 <div className="relative min-w-0 flex-1">
50 <Skeleton className="absolute left-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
51 <Skeleton className="absolute right-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
52 <div className="flex gap-1 overflow-hidden px-8 py-0.5">
53 <Skeleton className="h-6 w-16 shrink-0 rounded-md" />
54 <Skeleton className="h-6 w-24 shrink-0 rounded-md" />
55 <Skeleton className="h-6 w-24 shrink-0 rounded-md max-sm:hidden" />
56 </div>
57 </div>
58 <div className="flex shrink-0 gap-1">
59 <Skeleton className="size-7 rounded-md" />
60 <Skeleton className="size-7 rounded-md" />
61 </div>
62 </>
63 );
64}
65
66export function DiffPanelLoadingState(props: { label: string }) {
67 return (
68 <div className="flex min-h-0 flex-1 flex-col p-2">
69 <div
70 className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border/60 bg-card/25"
71 role="status"
72 aria-live="polite"
73 aria-label={props.label}
74 >
75 <div className="flex items-center gap-2 border-b border-border/50 px-3 py-2">
76 <Skeleton className="h-4 w-32 rounded-full" />
77 <Skeleton className="ml-auto h-4 w-20 rounded-full" />
78 </div>
79 <div className="flex min-h-0 flex-1 flex-col gap-4 px-3 py-4">
80 <div className="space-y-2">
81 <Skeleton className="h-3 w-full rounded-full" />
82 <Skeleton className="h-3 w-full rounded-full" />
83 <Skeleton className="h-3 w-10/12 rounded-full" />
84 <Skeleton className="h-3 w-11/12 rounded-full" />
85 <Skeleton className="h-3 w-9/12 rounded-full" />
86 </div>
87 <span className="sr-only">{props.label}</span>
88 </div>
89 </div>
90 </div>
91 );
92}
apps/web/src/routes/_chat.$threadId.tsx+42-20
...@@ -1,8 +1,15 @@...@@ -1,8 +1,15 @@
1import { ThreadId } from "@t3tools/contracts";1import { ThreadId } from "@t3tools/contracts";
2import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";2import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";
3import { Suspense, lazy, type ReactNode, useCallback, useEffect } from "react";3import { Suspense, lazy, type ReactNode, useCallback, useEffect, useState } from "react";
44
5import ChatView from "../components/ChatView";5import ChatView from "../components/ChatView";
6import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
7import {
8 DiffPanelHeaderSkeleton,
9 DiffPanelLoadingState,
10 DiffPanelShell,
11 type DiffPanelMode,
12} from "../components/DiffPanelShell";
6import { useComposerDraftStore } from "../composerDraftStore";13import { useComposerDraftStore } from "../composerDraftStore";
7import {14import {
8 type DiffRouteSearch,15 type DiffRouteSearch,
...@@ -47,19 +54,21 @@ const DiffPanelSheet = (props: {...@@ -47,19 +54,21 @@ const DiffPanelSheet = (props: {
47 );54 );
48};55};
4956
50const DiffLoadingFallback = (props: { inline: boolean }) => {57const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
51 if (props.inline) {58 return (
52 return (59 <DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}>
53 <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">60 <DiffPanelLoadingState label="Loading diff viewer..." />
54 Loading diff viewer...61 </DiffPanelShell>
55 </div>62 );
56 );63};
57 }
5864
65const LazyDiffPanel = (props: { mode: DiffPanelMode }) => {
59 return (66 return (
60 <aside className="flex h-full w-[560px] shrink-0 items-center justify-center border-l border-border bg-card px-4 text-center text-xs text-muted-foreground/70">67 <DiffWorkerPoolProvider>
61 Loading diff viewer...68 <Suspense fallback={<DiffLoadingFallback mode={props.mode} />}>
62 </aside>69 <DiffPanel mode={props.mode} />
70 </Suspense>
71 </DiffWorkerPoolProvider>
63 );72 );
64};73};
6574
...@@ -67,8 +76,9 @@ const DiffPanelInlineSidebar = (props: {...@@ -67,8 +76,9 @@ const DiffPanelInlineSidebar = (props: {
67 diffOpen: boolean;76 diffOpen: boolean;
68 onCloseDiff: () => void;77 onCloseDiff: () => void;
69 onOpenDiff: () => void;78 onOpenDiff: () => void;
79 renderDiffContent: boolean;
70}) => {80}) => {
71 const { diffOpen, onCloseDiff, onOpenDiff } = props;81 const { diffOpen, onCloseDiff, onOpenDiff, renderDiffContent } = props;
72 const onOpenChange = useCallback(82 const onOpenChange = useCallback(
73 (open: boolean) => {83 (open: boolean) => {
74 if (open) {84 if (open) {
...@@ -143,9 +153,7 @@ const DiffPanelInlineSidebar = (props: {...@@ -143,9 +153,7 @@ const DiffPanelInlineSidebar = (props: {
143 storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,153 storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,
144 }}154 }}
145 >155 >
146 <Suspense fallback={<DiffLoadingFallback inline />}>156 {renderDiffContent ? <LazyDiffPanel mode="sidebar" /> : null}
147 <DiffPanel mode="sidebar" />
148 </Suspense>
149 <SidebarRail />157 <SidebarRail />
150 </Sidebar>158 </Sidebar>
151 </SidebarProvider>159 </SidebarProvider>
...@@ -166,6 +174,9 @@ function ChatThreadRouteView() {...@@ -166,6 +174,9 @@ function ChatThreadRouteView() {
166 const routeThreadExists = threadExists || draftThreadExists;174 const routeThreadExists = threadExists || draftThreadExists;
167 const diffOpen = search.diff === "1";175 const diffOpen = search.diff === "1";
168 const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);176 const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
177 // TanStack Router keeps active route components mounted across param-only navigations
178 // unless remountDeps are configured, so this stays warm across thread switches.
179 const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);
169 const closeDiff = useCallback(() => {180 const closeDiff = useCallback(() => {
170 void navigate({181 void navigate({
171 to: "/$threadId",182 to: "/$threadId",
...@@ -184,6 +195,12 @@ function ChatThreadRouteView() {...@@ -184,6 +195,12 @@ function ChatThreadRouteView() {
184 });195 });
185 }, [navigate, threadId]);196 }, [navigate, threadId]);
186197
198 useEffect(() => {
199 if (diffOpen) {
200 setHasOpenedDiff(true);
201 }
202 }, [diffOpen]);
203
187 useEffect(() => {204 useEffect(() => {
188 if (!threadsHydrated) {205 if (!threadsHydrated) {
189 return;206 return;
...@@ -199,13 +216,20 @@ function ChatThreadRouteView() {...@@ -199,13 +216,20 @@ function ChatThreadRouteView() {
199 return null;216 return null;
200 }217 }
201218
219 const shouldRenderDiffContent = diffOpen || hasOpenedDiff;
220
202 if (!shouldUseDiffSheet) {221 if (!shouldUseDiffSheet) {
203 return (222 return (
204 <>223 <>
205 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">224 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
206 <ChatView key={threadId} threadId={threadId} />225 <ChatView key={threadId} threadId={threadId} />
207 </SidebarInset>226 </SidebarInset>
208 <DiffPanelInlineSidebar diffOpen={diffOpen} onCloseDiff={closeDiff} onOpenDiff={openDiff} />227 <DiffPanelInlineSidebar
228 diffOpen={diffOpen}
229 onCloseDiff={closeDiff}
230 onOpenDiff={openDiff}
231 renderDiffContent={shouldRenderDiffContent}
232 />
209 </>233 </>
210 );234 );
211 }235 }
...@@ -216,9 +240,7 @@ function ChatThreadRouteView() {...@@ -216,9 +240,7 @@ function ChatThreadRouteView() {
216 <ChatView key={threadId} threadId={threadId} />240 <ChatView key={threadId} threadId={threadId} />
217 </SidebarInset>241 </SidebarInset>
218 <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}>242 <DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}>
219 <Suspense fallback={<DiffLoadingFallback inline={false} />}>243 {shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null}
220 <DiffPanel mode="sheet" />
221 </Suspense>
222 </DiffPanelSheet>244 </DiffPanelSheet>
223 </>245 </>
224 );246 );
apps/web/src/routes/_chat.tsx+1-4
...@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";...@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";
3import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";3import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
4import { useEffect } from "react";4import { useEffect } from "react";
55
6import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
7import ThreadSidebar from "../components/Sidebar";6import ThreadSidebar from "../components/Sidebar";
8import { useHandleNewThread } from "../hooks/useHandleNewThread";7import { useHandleNewThread } from "../hooks/useHandleNewThread";
9import { isTerminalFocused } from "../lib/terminalFocus";8import { isTerminalFocused } from "../lib/terminalFocus";
...@@ -120,9 +119,7 @@ function ChatRouteLayout() {...@@ -120,9 +119,7 @@ function ChatRouteLayout() {
120 >119 >
121 <ThreadSidebar />120 <ThreadSidebar />
122 </Sidebar>121 </Sidebar>
123 <DiffWorkerPoolProvider>122 <Outlet />
124 <Outlet />
125 </DiffWorkerPoolProvider>
126 </SidebarProvider>123 </SidebarProvider>
127 );124 );
128}125}