From f19cd91b970abb291a5d36a77ea7fb8ef791d40d Mon Sep 17 00:00:00 2001 From: BinBandit Date: Thu, 12 Mar 2026 08:57:31 +1100 Subject: [PATCH 1/3] fix(web): defer diff worker startup until diff opens --- apps/web/src/routes/_chat.$threadId.tsx | 46 ++++++++++++++++++++----- apps/web/src/routes/_chat.tsx | 5 +-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index b85aeab0d9..c86380ac2a 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -1,8 +1,9 @@ import { ThreadId } from "@t3tools/contracts"; import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; -import { Suspense, lazy, type ReactNode, useCallback, useEffect } from "react"; +import { Suspense, lazy, type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import ChatView from "../components/ChatView"; +import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; import { useComposerDraftStore } from "../composerDraftStore"; import { type DiffRouteSearch, @@ -63,12 +64,23 @@ const DiffLoadingFallback = (props: { inline: boolean }) => { ); }; +const LazyDiffPanel = (props: { inline: boolean; mode: "sheet" | "sidebar" }) => { + return ( + + }> + + + + ); +}; + const DiffPanelInlineSidebar = (props: { diffOpen: boolean; onCloseDiff: () => void; onOpenDiff: () => void; + renderDiffContent: boolean; }) => { - const { diffOpen, onCloseDiff, onOpenDiff } = props; + const { diffOpen, onCloseDiff, onOpenDiff, renderDiffContent } = props; const onOpenChange = useCallback( (open: boolean) => { if (open) { @@ -143,9 +155,7 @@ const DiffPanelInlineSidebar = (props: { storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY, }} > - }> - - + {renderDiffContent ? : null} @@ -166,6 +176,8 @@ function ChatThreadRouteView() { const routeThreadExists = threadExists || draftThreadExists; const diffOpen = search.diff === "1"; const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen); + const previousThreadIdRef = useRef(threadId); const closeDiff = useCallback(() => { void navigate({ to: "/$threadId", @@ -186,6 +198,17 @@ function ChatThreadRouteView() { }); }, [navigate, threadId]); + useEffect(() => { + if (previousThreadIdRef.current !== threadId) { + previousThreadIdRef.current = threadId; + setHasOpenedDiff(diffOpen); + return; + } + if (diffOpen) { + setHasOpenedDiff(true); + } + }, [diffOpen, threadId]); + useEffect(() => { if (!threadsHydrated) { return; @@ -201,13 +224,20 @@ function ChatThreadRouteView() { return null; } + const shouldRenderDiffContent = diffOpen || hasOpenedDiff; + if (!shouldUseDiffSheet) { return ( <> - + ); } @@ -218,9 +248,7 @@ function ChatThreadRouteView() { - }> - - + {shouldRenderDiffContent ? : null} ); diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 0d7f1724b2..7b0f9a75d5 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -1,7 +1,6 @@ import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect } from "react"; -import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; import ThreadSidebar from "../components/Sidebar"; import { Sidebar, SidebarProvider } from "~/components/ui/sidebar"; @@ -33,9 +32,7 @@ function ChatRouteLayout() { > - - - + ); } From cec7947d041d52e205d01a8f5db3f6b9999d5cc3 Mon Sep 17 00:00:00 2001 From: BinBandit Date: Fri, 13 Mar 2026 10:48:47 +1100 Subject: [PATCH 2/3] fix(web): reuse deferred diff worker across threads --- apps/web/src/routes/_chat.$threadId.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index c86380ac2a..3866411f26 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -1,6 +1,6 @@ import { ThreadId } from "@t3tools/contracts"; import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router"; -import { Suspense, lazy, type ReactNode, useCallback, useEffect, useRef, useState } from "react"; +import { Suspense, lazy, type ReactNode, useCallback, useEffect, useState } from "react"; import ChatView from "../components/ChatView"; import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; @@ -176,8 +176,9 @@ function ChatThreadRouteView() { const routeThreadExists = threadExists || draftThreadExists; const diffOpen = search.diff === "1"; const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + // TanStack Router keeps active route components mounted across param-only navigations + // unless remountDeps are configured, so this stays warm across thread switches. const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen); - const previousThreadIdRef = useRef(threadId); const closeDiff = useCallback(() => { void navigate({ to: "/$threadId", @@ -199,15 +200,10 @@ function ChatThreadRouteView() { }, [navigate, threadId]); useEffect(() => { - if (previousThreadIdRef.current !== threadId) { - previousThreadIdRef.current = threadId; - setHasOpenedDiff(diffOpen); - return; - } if (diffOpen) { setHasOpenedDiff(true); } - }, [diffOpen, threadId]); + }, [diffOpen]); useEffect(() => { if (!threadsHydrated) { From a532709a42a1d04a797b62663dacdd791810747d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Mar 2026 20:40:24 -0700 Subject: [PATCH 3/3] fix loading --- apps/web/src/components/DiffPanel.tsx | 44 +++-------- apps/web/src/components/DiffPanelShell.tsx | 92 ++++++++++++++++++++++ apps/web/src/routes/_chat.$threadId.tsx | 30 ++++--- 3 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 apps/web/src/components/DiffPanelShell.tsx diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 6a3c008c51..34ad788814 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -19,7 +19,6 @@ import { cn } from "~/lib/utils"; import { readNativeApi } from "../nativeApi"; import { resolvePathLinkTarget } from "../terminal-links"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; -import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; import { buildPatchCacheKey } from "../lib/diffRendering"; import { resolveDiffThemeName } from "../lib/diffRendering"; @@ -27,6 +26,7 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useStore } from "../store"; import { useAppSettings } from "../appSettings"; import { formatShortTimestamp } from "../timestampFormat"; +import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { ToggleGroup, Toggle } from "./ui/toggle-group"; type DiffRenderMode = "stacked" | "split"; @@ -152,7 +152,7 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { } interface DiffPanelProps { - mode?: "inline" | "sheet" | "sidebar"; + mode?: DiffPanelMode; } export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider"; @@ -398,7 +398,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" }); }, [selectedTurn?.turnId, selectedTurnId]); - const shouldUseDragRegion = isElectron && mode !== "sheet"; const headerRow = ( <>
@@ -512,28 +511,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { ); - const headerRowClassName = cn( - "flex items-center justify-between gap-2 px-4", - shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12", - ); return ( -
- {shouldUseDragRegion ? ( -
{headerRow}
- ) : ( -
-
{headerRow}
-
- )} - + {!activeThread ? (
Select a thread to inspect turn diffs. @@ -558,15 +538,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
)} {!renderablePatch ? ( -
-

- {isLoadingCheckpointDiff - ? "Loading checkpoint diff..." - : hasNoNetChanges + isLoadingCheckpointDiff ? ( + + ) : ( +

+

+ {hasNoNetChanges ? "No net changes in this selection." : "No patch available for this selection."} -

-
+

+
+ ) ) : renderablePatch.kind === "files" ? ( )} -
+ ); } diff --git a/apps/web/src/components/DiffPanelShell.tsx b/apps/web/src/components/DiffPanelShell.tsx new file mode 100644 index 0000000000..c08c53325d --- /dev/null +++ b/apps/web/src/components/DiffPanelShell.tsx @@ -0,0 +1,92 @@ +import type { ReactNode } from "react"; + +import { isElectron } from "~/env"; +import { cn } from "~/lib/utils"; + +import { Skeleton } from "./ui/skeleton"; + +export type DiffPanelMode = "inline" | "sheet" | "sidebar"; + +function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) { + const shouldUseDragRegion = isElectron && mode !== "sheet"; + return cn( + "flex items-center justify-between gap-2 px-4", + shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12", + ); +} + +export function DiffPanelShell(props: { + mode: DiffPanelMode; + header: ReactNode; + children: ReactNode; +}) { + const shouldUseDragRegion = isElectron && props.mode !== "sheet"; + + return ( +
+ {shouldUseDragRegion ? ( +
{props.header}
+ ) : ( +
+
{props.header}
+
+ )} + {props.children} +
+ ); +} + +export function DiffPanelHeaderSkeleton() { + return ( + <> +
+ + +
+ + + +
+
+
+ + +
+ + ); +} + +export function DiffPanelLoadingState(props: { label: string }) { + return ( +
+
+
+ + +
+
+
+ + + + + +
+ {props.label} +
+
+
+ ); +} diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx index 3c6ea9422e..d7dfd56a7c 100644 --- a/apps/web/src/routes/_chat.$threadId.tsx +++ b/apps/web/src/routes/_chat.$threadId.tsx @@ -4,6 +4,12 @@ import { Suspense, lazy, type ReactNode, useCallback, useEffect, useState } from import ChatView from "../components/ChatView"; import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider"; +import { + DiffPanelHeaderSkeleton, + DiffPanelLoadingState, + DiffPanelShell, + type DiffPanelMode, +} from "../components/DiffPanelShell"; import { useComposerDraftStore } from "../composerDraftStore"; import { type DiffRouteSearch, @@ -48,26 +54,18 @@ const DiffPanelSheet = (props: { ); }; -const DiffLoadingFallback = (props: { inline: boolean }) => { - if (props.inline) { - return ( -
- Loading diff viewer... -
- ); - } - +const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => { return ( - + }> + + ); }; -const LazyDiffPanel = (props: { inline: boolean; mode: "sheet" | "sidebar" }) => { +const LazyDiffPanel = (props: { mode: DiffPanelMode }) => { return ( - }> + }> @@ -155,7 +153,7 @@ const DiffPanelInlineSidebar = (props: { storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY, }} > - {renderDiffContent ? : null} + {renderDiffContent ? : null} @@ -242,7 +240,7 @@ function ChatThreadRouteView() { - {shouldRenderDiffContent ? : null} + {shouldRenderDiffContent ? : null} );