import { AgentSidebar } from "@agent-native/core/client/agent-chat"; 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 { useEffect, useMemo, useState } from "react"; import { useLocation } from "react-router"; import { useDecks } from "@/context/DeckContext"; import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed"; import { TAB_ID } from "@/lib/tab-id"; import { cn } from "@/lib/utils"; import { AgentWorkIndicator } from "./AgentWorkIndicator"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; interface LayoutProps { children: React.ReactNode; } /** Routes whose pages render their own toolbar — Layout still renders chrome * (sidebar + AgentSidebar wrapper) but skips its own Header. */ function pageHasOwnToolbar(pathname: string): boolean { if (pathname.startsWith("/deck/")) return true; // /extensions (list) and /extensions/ (viewer) both render their own headers // from @agent-native/core/client/extensions. if (pathname === "/extensions" || pathname.startsWith("/extensions/")) return true; return false; } export function Layout({ children }: LayoutProps) { const location = useLocation(); const t = useT(); const [sidebarOpen, setSidebarOpen] = useState(false); const { collapsed: sidebarCollapsed, setCollapsed: setSidebarCollapsed } = useSidebarCollapsed(); const { getDeck } = useDecks(); // Scope new chats to the deck the user is currently editing. The route // is `/deck/:id`; everywhere else (list, presentation) leaves // scope null so chats stay in the general pool. Falling back to the // raw deck id keeps the chat bound even before the deck object has // streamed in — once the title arrives the badge updates in place. const deckScope = useMemo(() => { const match = location.pathname.match(/^\/deck\/([^/]+)/); const deckId = match?.[1]; if (!deckId) return null; const deck = getDeck(deckId); return { type: "deck" as const, id: deckId, label: deck?.title || "" }; }, [location.pathname, getDeck]); useEffect(() => { setSidebarOpen(false); }, [location.pathname]); useEffect(() => { const onResize = () => { if (window.innerWidth >= 768) setSidebarOpen(false); }; window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); const ownToolbar = pageHasOwnToolbar(location.pathname); return ( } >
{sidebarOpen && (
setSidebarOpen(false)} /> )}
setSidebarCollapsed((prev) => !prev) } />
{/* Mobile-only nav strip with hamburger — only when there's no page toolbar */} {!ownToolbar && (
)} {!ownToolbar &&
}
{children}
); }