import { AgentSidebar, useGuidedQuestionFlow, } from "@agent-native/core/client/agent-chat"; import { getBrowserTabId, useSession } from "@agent-native/core/client/hooks"; import { isEmbedAuthActive } from "@agent-native/core/client/host"; import { useT } from "@agent-native/core/client/i18n"; import { CreativeContextComposerChip } from "@agent-native/creative-context/client"; import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell"; import { IconMenu2 } from "@tabler/icons-react"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react"; import { useLocation } from "react-router"; import { useNavigationState } from "@/hooks/use-navigation-state"; import { DESIGN_CHAT_STORAGE_KEY } from "@/lib/agent-chat"; import { cn } from "@/lib/utils"; import { FigmaLinkComposerBubble, useDetectedFigmaComposerLink, } from "../editor/FigmaLinkComposerBubble"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; interface LayoutProps { children: React.ReactNode; } const MobileSidebarContext = createContext<(() => void) | null>(null); export function useOpenMobileSidebar() { return useContext(MobileSidebarContext); } /** Routes that render with no app shell at all (no sidebar, no header). */ const BARE_PREFIXES = ["/present/"]; /** * Routes where the page renders its own toolbar instead of the global Header. * The Header is hidden so the page can supply richer custom chrome (e.g. * DesignEditor mode/zoom/device, shared ExtensionViewer / ExtensionsListPage * chrome). The editor owns its agent surface inside its Figma-style left rail. */ const EDITOR_PREFIXES = ["/design/", "/extensions"]; export function Layout({ children }: LayoutProps) { const location = useLocation(); const t = useT(); const { session } = useSession(); const hasSession = Boolean(session?.email); const embedded = isEmbedAuthActive(); useNavigationState(hasSession); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const openMobileSidebar = useCallback(() => setMobileSidebarOpen(true), []); const isDesignEditor = location.pathname.startsWith("/design/"); const showMobileTopBar = !isDesignEditor; const browserTabId = getBrowserTabId(); const { link: detectedFigmaComposerLink, onComposerTextChange: handleComposerTextChange, } = useDetectedFigmaComposerLink(); // Bind chat to the currently-open design. Same pattern as slides — the // route is `/design/:id` for the editor and `/present/:id` for preview // (which we already short-circuit as BARE). Anywhere else (list, // design-systems, settings) leaves scope null so general chats keep working. const designScope = useMemo(() => { const match = location.pathname.match(/^\/design\/([^/]+)/); const designId = match?.[1]; if (!designId) return null; return { type: "design" as const, id: designId }; }, [location.pathname]); const designQuestionStateKey = designScope ? `show-questions:${designScope.id}` : "show-questions"; const { questions: pendingDesignQuestions } = useGuidedQuestionFlow({ enabled: hasSession, stateKey: designQuestionStateKey, queryKey: [designQuestionStateKey], browserTabId, refetchInterval: embedded || !isDesignEditor || !hasSession ? false : 2000, }); const designQuestionsWaitingSlot = isDesignEditor && pendingDesignQuestions?.length ? (