import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";
import { Suspense, lazy, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import ChatView from "../components/ChatView";
import { threadHasStarted } from "../components/ChatView.logic";
import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
import {
DiffPanelHeaderSkeleton,
DiffPanelLoadingState,
DiffPanelShell,
type DiffPanelMode,
} from "../components/DiffPanelShell";
import { finalizePromotedDraftThreadByRef, useComposerDraftStore } from "../composerDraftStore";
import {
type DiffRouteSearch,
parseDiffRouteSearch,
stripDiffSearchParams,
} from "../diffRouteSearch";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { selectEnvironmentState, selectThreadByRef, useStore } from "../store";
import { createThreadSelectorByRef } from "../storeSelectors";
import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes";
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 navigate = useNavigate();
const threadRef = Route.useParams({
select: (params) => resolveThreadRouteRef(params),
});
const search = Route.useSearch();
const bootstrapComplete = useStore(
(store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).bootstrapComplete,
);
const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]));
const threadExists = useStore((store) => selectThreadByRef(store, threadRef) !== undefined);
const environmentHasServerThreads = useStore(
(store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).threadIds.length > 0,
);
const draftThreadExists = useComposerDraftStore((store) =>
threadRef ? store.getDraftThreadByRef(threadRef) !== null : false,
);
const draftThread = useComposerDraftStore((store) =>
threadRef ? store.getDraftThreadByRef(threadRef) : null,
);
const environmentHasDraftThreads = useComposerDraftStore((store) => {
if (!threadRef) {
return false;
}
return store.hasDraftThreadsInEnvironment(threadRef.environmentId);
});
const routeThreadExists = threadExists || draftThreadExists;
const serverThreadStarted = threadHasStarted(serverThread);
const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads;
const diffOpen = search.diff === "1";
const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);
const closeDiff = useCallback(() => {
if (!threadRef) {
return;
}
void navigate({
to: "/$environmentId/$threadId",
params: buildThreadRouteParams(threadRef),
search: { diff: undefined },
});
}, [navigate, threadRef]);
const openDiff = useCallback(() => {
if (!threadRef) {
return;
}
void navigate({
to: "/$environmentId/$threadId",
params: buildThreadRouteParams(threadRef),
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return { ...rest, diff: "1" };
},
});
}, [navigate, threadRef]);
useEffect(() => {
if (diffOpen) {
setHasOpenedDiff(true);
}
}, [diffOpen]);
useEffect(() => {
if (!threadRef || !bootstrapComplete) {
return;
}
if (!routeThreadExists && environmentHasAnyThreads) {
void navigate({ to: "/", replace: true });
}
}, [bootstrapComplete, environmentHasAnyThreads, navigate, routeThreadExists, threadRef]);
useEffect(() => {
if (!threadRef || !serverThreadStarted || !draftThread?.promotedTo) {
return;
}
finalizePromotedDraftThreadByRef(threadRef);
}, [draftThread?.promotedTo, serverThreadStarted, threadRef]);
if (!threadRef || !bootstrapComplete || !routeThreadExists) {
return null;
}
const shouldRenderDiffContent = diffOpen || hasOpenedDiff;
if (!shouldUseDiffSheet) {
return (
<>
>
);
}
return (
<>
{shouldRenderDiffContent ? : null}
>
);
}
export const Route = createFileRoute("/_chat/$environmentId/$threadId")({
validateSearch: (search) => parseDiffRouteSearch(search),
search: {
middlewares: [retainSearchParams(["diff"])],
},
component: ChatThreadRouteView,
});