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}
- ) : (
-
- )}
-
+
{!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.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 5e05b51fbd..d7dfd56a7c 100644
--- a/apps/web/src/routes/_chat.$threadId.tsx
+++ b/apps/web/src/routes/_chat.$threadId.tsx
@@ -1,8 +1,15 @@
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, useState } from "react";
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,
@@ -47,19 +54,21 @@ const DiffPanelSheet = (props: {
);
};
-const DiffLoadingFallback = (props: { inline: boolean }) => {
- if (props.inline) {
- return (
-
- Loading diff viewer...
-
- );
- }
+const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
+ return (
+
}>
+
+
+ );
+};
+const LazyDiffPanel = (props: { mode: DiffPanelMode }) => {
return (
-
+
+ }>
+
+
+
);
};
@@ -67,8 +76,9 @@ 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 +153,7 @@ const DiffPanelInlineSidebar = (props: {
storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,
}}
>
-
}>
-
-
+ {renderDiffContent ?
: null}
@@ -166,6 +174,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 closeDiff = useCallback(() => {
void navigate({
to: "/$threadId",
@@ -184,6 +195,12 @@ function ChatThreadRouteView() {
});
}, [navigate, threadId]);
+ useEffect(() => {
+ if (diffOpen) {
+ setHasOpenedDiff(true);
+ }
+ }, [diffOpen]);
+
useEffect(() => {
if (!threadsHydrated) {
return;
@@ -199,13 +216,20 @@ function ChatThreadRouteView() {
return null;
}
+ const shouldRenderDiffContent = diffOpen || hasOpenedDiff;
+
if (!shouldUseDiffSheet) {
return (
<>
-
+
>
);
}
@@ -216,9 +240,7 @@ function ChatThreadRouteView() {
- }>
-
-
+ {shouldRenderDiffContent ? : null}
>
);
diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx
index 6014bc5f23..193cb0e7a9 100644
--- a/apps/web/src/routes/_chat.tsx
+++ b/apps/web/src/routes/_chat.tsx
@@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query";
import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
-import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
import ThreadSidebar from "../components/Sidebar";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { isTerminalFocused } from "../lib/terminalFocus";
@@ -120,9 +119,7 @@ function ChatRouteLayout() {
>
-
-
-
+
);
}