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, });