Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 13 additions & 31 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ 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";
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";
Expand Down Expand Up @@ -152,7 +152,7 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
}

interface DiffPanelProps {
mode?: "inline" | "sheet" | "sidebar";
mode?: DiffPanelMode;
}

export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
Expand Down Expand Up @@ -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 = (
<>
<div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]">
Expand Down Expand Up @@ -512,28 +511,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
</ToggleGroup>
</>
);
const headerRowClassName = cn(
"flex items-center justify-between gap-2 px-4",
shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
);

return (
<div
className={cn(
"flex h-full min-w-0 flex-col bg-background",
mode === "inline"
? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
: "w-full",
)}
>
{shouldUseDragRegion ? (
<div className={headerRowClassName}>{headerRow}</div>
) : (
<div className="border-b border-border">
<div className={headerRowClassName}>{headerRow}</div>
</div>
)}

<DiffPanelShell mode={mode} header={headerRow}>
{!activeThread ? (
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
Select a thread to inspect turn diffs.
Expand All @@ -558,15 +538,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
</div>
)}
{!renderablePatch ? (
<div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
<p>
{isLoadingCheckpointDiff
? "Loading checkpoint diff..."
: hasNoNetChanges
isLoadingCheckpointDiff ? (
<DiffPanelLoadingState label="Loading checkpoint diff..." />
) : (
<div className="flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70">
<p>
{hasNoNetChanges
? "No net changes in this selection."
: "No patch available for this selection."}
</p>
</div>
</p>
</div>
)
) : renderablePatch.kind === "files" ? (
<Virtualizer
className="diff-render-surface h-full min-h-0 overflow-auto px-2 pb-2"
Expand Down Expand Up @@ -622,6 +604,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
</div>
</>
)}
</div>
</DiffPanelShell>
);
}
92 changes: 92 additions & 0 deletions apps/web/src/components/DiffPanelShell.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
"flex h-full min-w-0 flex-col bg-background",
props.mode === "inline"
? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border"
: "w-full",
)}
>
{shouldUseDragRegion ? (
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
) : (
<div className="border-b border-border">
<div className={getDiffPanelHeaderRowClassName(props.mode)}>{props.header}</div>
</div>
)}
{props.children}
</div>
);
}

export function DiffPanelHeaderSkeleton() {
return (
<>
<div className="relative min-w-0 flex-1">
<Skeleton className="absolute left-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
<Skeleton className="absolute right-0 top-1/2 size-6 -translate-y-1/2 rounded-md border border-border/50" />
<div className="flex gap-1 overflow-hidden px-8 py-0.5">
<Skeleton className="h-6 w-16 shrink-0 rounded-md" />
<Skeleton className="h-6 w-24 shrink-0 rounded-md" />
<Skeleton className="h-6 w-24 shrink-0 rounded-md max-sm:hidden" />
</div>
</div>
<div className="flex shrink-0 gap-1">
<Skeleton className="size-7 rounded-md" />
<Skeleton className="size-7 rounded-md" />
</div>
</>
);
}

export function DiffPanelLoadingState(props: { label: string }) {
return (
<div className="flex min-h-0 flex-1 flex-col p-2">
<div
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border/60 bg-card/25"
role="status"
aria-live="polite"
aria-label={props.label}
>
<div className="flex items-center gap-2 border-b border-border/50 px-3 py-2">
<Skeleton className="h-4 w-32 rounded-full" />
<Skeleton className="ml-auto h-4 w-20 rounded-full" />
</div>
<div className="flex min-h-0 flex-1 flex-col gap-4 px-3 py-4">
<div className="space-y-2">
<Skeleton className="h-3 w-full rounded-full" />
<Skeleton className="h-3 w-full rounded-full" />
<Skeleton className="h-3 w-10/12 rounded-full" />
<Skeleton className="h-3 w-11/12 rounded-full" />
<Skeleton className="h-3 w-9/12 rounded-full" />
</div>
<span className="sr-only">{props.label}</span>
</div>
</div>
</div>
);
}
62 changes: 42 additions & 20 deletions apps/web/src/routes/_chat.$threadId.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -47,28 +54,31 @@ const DiffPanelSheet = (props: {
);
};

const DiffLoadingFallback = (props: { inline: boolean }) => {
if (props.inline) {
return (
<div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
Loading diff viewer...
</div>
);
}
const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
return (
<DiffPanelShell mode={props.mode} header={<DiffPanelHeaderSkeleton />}>
<DiffPanelLoadingState label="Loading diff viewer..." />
</DiffPanelShell>
);
};

const LazyDiffPanel = (props: { mode: DiffPanelMode }) => {
return (
<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">
Loading diff viewer...
</aside>
<DiffWorkerPoolProvider>
<Suspense fallback={<DiffLoadingFallback mode={props.mode} />}>
<DiffPanel mode={props.mode} />
</Suspense>
</DiffWorkerPoolProvider>
);
};

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) {
Expand Down Expand Up @@ -143,9 +153,7 @@ const DiffPanelInlineSidebar = (props: {
storageKey: DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY,
}}
>
<Suspense fallback={<DiffLoadingFallback inline />}>
<DiffPanel mode="sidebar" />
</Suspense>
{renderDiffContent ? <LazyDiffPanel mode="sidebar" /> : null}
<SidebarRail />
</Sidebar>
</SidebarProvider>
Expand All @@ -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",
Expand All @@ -184,6 +195,12 @@ function ChatThreadRouteView() {
});
}, [navigate, threadId]);

useEffect(() => {
if (diffOpen) {
setHasOpenedDiff(true);
}
}, [diffOpen]);

useEffect(() => {
if (!threadsHydrated) {
return;
Expand All @@ -199,13 +216,20 @@ function ChatThreadRouteView() {
return null;
}

const shouldRenderDiffContent = diffOpen || hasOpenedDiff;

if (!shouldUseDiffSheet) {
return (
<>
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
<ChatView key={threadId} threadId={threadId} />
</SidebarInset>
<DiffPanelInlineSidebar diffOpen={diffOpen} onCloseDiff={closeDiff} onOpenDiff={openDiff} />
<DiffPanelInlineSidebar
diffOpen={diffOpen}
onCloseDiff={closeDiff}
onOpenDiff={openDiff}
renderDiffContent={shouldRenderDiffContent}
/>
</>
);
}
Expand All @@ -216,9 +240,7 @@ function ChatThreadRouteView() {
<ChatView key={threadId} threadId={threadId} />
</SidebarInset>
<DiffPanelSheet diffOpen={diffOpen} onCloseDiff={closeDiff}>
<Suspense fallback={<DiffLoadingFallback inline={false} />}>
<DiffPanel mode="sheet" />
</Suspense>
{shouldRenderDiffContent ? <LazyDiffPanel mode="sheet" /> : null}
</DiffPanelSheet>
</>
);
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -120,9 +119,7 @@ function ChatRouteLayout() {
>
<ThreadSidebar />
</Sidebar>
<DiffWorkerPoolProvider>
<Outlet />
</DiffWorkerPoolProvider>
<Outlet />
</SidebarProvider>
);
}
Expand Down
Loading