import { ThreadId } from "@t3tools/contracts";
import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";
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,
parseDiffRouteSearch,
stripDiffSearchParams,
} from "../diffRouteSearch";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { useStore } from "../store";
import { Sheet, SheetPopup } from "../components/ui/sheet";
import { Sidebar, SidebarInset, SidebarProvider, SidebarRail } from "~/components/ui/sidebar";
const DiffPanel = lazy(() => import("../components/DiffPanel"));
const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";
const DIFF_INLINE_DEFAULT_WIDTH = "clamp(28rem,48vw,44rem)";
const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 26 * 16;
const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208;
const DiffPanelSheet = (props: {
children: ReactNode;
diffOpen: boolean;
onCloseDiff: () => void;
}) => {
return (
{
if (!open) {
props.onCloseDiff();
}
}}
>
{props.children}
);
};
const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
return (
}>
);
};
const LazyDiffPanel = (props: { mode: DiffPanelMode }) => {
return (
}>
);
};
const DiffPanelInlineSidebar = (props: {
diffOpen: boolean;
onCloseDiff: () => void;
onOpenDiff: () => void;
renderDiffContent: boolean;
}) => {
const { diffOpen, onCloseDiff, onOpenDiff, renderDiffContent } = props;
const onOpenChange = useCallback(
(open: boolean) => {
if (open) {
onOpenDiff();
return;
}
onCloseDiff();
},
[onCloseDiff, onOpenDiff],
);
const shouldAcceptInlineSidebarWidth = useCallback(
({ nextWidth, wrapper }: { nextWidth: number; wrapper: HTMLElement }) => {
const composerForm = document.querySelector("[data-chat-composer-form='true']");
if (!composerForm) return true;
const composerViewport = composerForm.parentElement;
if (!composerViewport) return true;
const previousSidebarWidth = wrapper.style.getPropertyValue("--sidebar-width");
wrapper.style.setProperty("--sidebar-width", `${nextWidth}px`);
const viewportStyle = window.getComputedStyle(composerViewport);
const viewportPaddingLeft = Number.parseFloat(viewportStyle.paddingLeft) || 0;
const viewportPaddingRight = Number.parseFloat(viewportStyle.paddingRight) || 0;
const viewportContentWidth = Math.max(
0,
composerViewport.clientWidth - viewportPaddingLeft - viewportPaddingRight,
);
const formRect = composerForm.getBoundingClientRect();
const composerFooter = composerForm.querySelector(
"[data-chat-composer-footer='true']",
);
const composerRightActions = composerForm.querySelector(
"[data-chat-composer-actions='right']",
);
const composerRightActionsWidth = composerRightActions?.getBoundingClientRect().width ?? 0;
const composerFooterGap = composerFooter
? Number.parseFloat(window.getComputedStyle(composerFooter).columnGap) ||
Number.parseFloat(window.getComputedStyle(composerFooter).gap) ||
0
: 0;
const minimumComposerWidth =
COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX + composerRightActionsWidth + composerFooterGap;
const hasComposerOverflow = composerForm.scrollWidth > composerForm.clientWidth + 0.5;
const overflowsViewport = formRect.width > viewportContentWidth + 0.5;
const violatesMinimumComposerWidth = composerForm.clientWidth + 0.5 < minimumComposerWidth;
if (previousSidebarWidth.length > 0) {
wrapper.style.setProperty("--sidebar-width", previousSidebarWidth);
} else {
wrapper.style.removeProperty("--sidebar-width");
}
return !hasComposerOverflow && !overflowsViewport && !violatesMinimumComposerWidth;
},
[],
);
return (
{renderDiffContent ? : null}
);
};
function ChatThreadRouteView() {
const bootstrapComplete = useStore((store) => store.bootstrapComplete);
const navigate = useNavigate();
const threadId = Route.useParams({
select: (params) => ThreadId.makeUnsafe(params.threadId),
});
const search = Route.useSearch();
const threadExists = useStore((store) => store.threads.some((thread) => thread.id === threadId));
const draftThreadExists = useComposerDraftStore((store) =>
Object.hasOwn(store.draftThreadsByThreadId, threadId),
);
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",
params: { threadId },
search: { diff: undefined },
});
}, [navigate, threadId]);
const openDiff = useCallback(() => {
void navigate({
to: "/$threadId",
params: { threadId },
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return { ...rest, diff: "1" };
},
});
}, [navigate, threadId]);
useEffect(() => {
if (diffOpen) {
setHasOpenedDiff(true);
}
}, [diffOpen]);
useEffect(() => {
if (!bootstrapComplete) {
return;
}
if (!routeThreadExists) {
void navigate({ to: "/", replace: true });
return;
}
}, [bootstrapComplete, navigate, routeThreadExists, threadId]);
if (!bootstrapComplete || !routeThreadExists) {
return null;
}
const shouldRenderDiffContent = diffOpen || hasOpenedDiff;
if (!shouldUseDiffSheet) {
return (
<>
>
);
}
return (
<>
{shouldRenderDiffContent ? : null}
>
);
}
export const Route = createFileRoute("/_chat/$threadId")({
validateSearch: (search) => parseDiffRouteSearch(search),
search: {
middlewares: [retainSearchParams(["diff"])],
},
component: ChatThreadRouteView,
});