import { AgentSidebar } from "@agent-native/core/client/agent-chat"; import { getBrowserTabId } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { InvitationBanner } from "@agent-native/core/client/org"; import { CreativeContextComposerChip } from "@agent-native/creative-context/client"; import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell"; import { IconMenu2 } from "@tabler/icons-react"; import { type CSSProperties, ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; import { useLocation, useNavigation } from "react-router"; import { DocumentEditorSkeleton } from "@/components/editor/DocumentEditorSkeleton"; import { DocumentSidebar } from "@/components/sidebar/DocumentSidebar"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { useCreatePage } from "@/hooks/use-create-page"; import { useIsMobile } from "@/hooks/use-mobile"; import { Header } from "./Header"; const SIDEBAR_WIDTH_KEY = "sidebar-width"; const DEFAULT_SIDEBAR_WIDTH = 240; const MIN_SIDEBAR_WIDTH = 240; const MAX_SIDEBAR_WIDTH = 480; const NARROW_DESKTOP_QUERY = "(max-width: 1099px)"; // Routes whose page renders its own custom toolbar (with AgentToggleButton). // Layout still mounts Sidebar + AgentSidebar, but skips its own Header so // there's no double-header. const NO_HEADER_PREFIXES = ["/page/", "/extensions"]; function loadSidebarWidth(): number { try { const stored = localStorage.getItem(SIDEBAR_WIDTH_KEY); if (stored) { const w = Number(stored); if (w >= MIN_SIDEBAR_WIDTH && w <= MAX_SIDEBAR_WIDTH) return w; } } catch {} return DEFAULT_SIDEBAR_WIDTH; } function useIsNarrowDesktop() { const [isNarrow, setIsNarrow] = useState( () => typeof window !== "undefined" && window.matchMedia(NARROW_DESKTOP_QUERY).matches, ); useEffect(() => { const media = window.matchMedia(NARROW_DESKTOP_QUERY); const update = () => setIsNarrow(media.matches); update(); media.addEventListener("change", update); return () => media.removeEventListener("change", update); }, []); return isNarrow; } export function documentPageIdFromPathname(pathname: string) { return pathname.match(/^\/page\/(.+)/)?.[1] ?? null; } interface LayoutProps { children: ReactNode; } export function Layout({ children }: LayoutProps) { const location = useLocation(); const navigation = useNavigation(); const pendingPathname = navigation.location?.pathname ?? null; const chromePathname = pendingPathname ?? location.pathname; const t = useT(); const currentDocumentId = documentPageIdFromPathname(location.pathname); const pendingDocumentId = pendingPathname ? documentPageIdFromPathname(pendingPathname) : null; const activeDocumentId = pendingDocumentId ?? currentDocumentId; const showPendingDocumentSkeleton = !!pendingDocumentId && pendingDocumentId !== currentDocumentId; // Bind chat to the currently-open document. Everywhere else (list view, // settings) leaves scope null so general chats stay available. const documentScope = useMemo( () => activeDocumentId ? { type: "document" as const, id: activeDocumentId } : null, [activeDocumentId], ); const isMobile = useIsMobile(); const isNarrowDesktop = useIsNarrowDesktop(); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [sidebarWidth, setSidebarWidth] = useState(loadSidebarWidth); const handleSidebarResize = useCallback((width: number) => { const clamped = Math.max( MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, width), ); setSidebarWidth(clamped); localStorage.setItem(SIDEBAR_WIDTH_KEY, String(clamped)); }, []); const showHeader = !NO_HEADER_PREFIXES.some((prefix) => chromePathname.startsWith(prefix), ); const createPage = useCreatePage({ awaitPersist: false }); useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (!(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return; if (e.key !== "n" && e.key !== "N") return; const target = e.target as HTMLElement | null; if (target?.isContentEditable) return; const tag = target?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; e.preventDefault(); void createPage(); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [createPage]); useEffect(() => { if (isNarrowDesktop) { window.dispatchEvent(new Event("agent-panel:close")); } }, [isNarrowDesktop]); const mobileSidebarTrigger = isMobile ? ( ) : null; const contentSidebarWidth = isMobile ? 0 : sidebarCollapsed ? 48 : sidebarWidth; return (
{isMobile ? ( <> setMobileSidebarOpen(false)} onNavigate={() => setMobileSidebarOpen(false)} /> {showHeader ? null : ( )} ) : (
setSidebarCollapsed((c) => !c)} width={sidebarWidth} onResize={handleSidebarResize} />
)} } >
{showHeader ? (
) : null} div]:flex-wrap [&>div]:items-start [&>div>span]:min-w-0 [&>div>span]:flex-1`} /> {showPendingDocumentSkeleton ? ( ) : ( children )}
); }