=================================================================== --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -1,221 +1,28 @@ -import { useT } from "@agent-native/core/client/i18n"; -import { - HeaderActionsProvider, - useHeaderActions, - useHeaderTitle, -} from "@agent-native/toolkit/app-shell/header-actions"; -import { - isAgentChatHomeHandoffActive, - useAgentChatHomeHandoff, - useAgentChatHomeHandoffLinks, -} from "@agent-native/toolkit/app/chat/agentkit-chat/rail"; -import { - RealtimeVoiceModeProvider, - useRealtimeVoiceModeOptional, -} from "@agent-native/toolkit/app/chat/composer"; -import { isSettingsPathname } from "@agent-native/toolkit/app/settings"; -import { lazy, Suspense, useEffect, useRef, useState } from "react"; -import { useLocation } from "react-router"; +import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetTitle, -} from "@/components/ui/sheet"; -import { APP_TITLE } from "@/lib/app-config"; -import { HOME_PATH, isChatPathname } from "@/lib/chat-paths"; -import { TAB_ID } from "@/lib/tab-id"; - -// Not lazy() like Header: the inspector wraps the page, so a Suspense fallback -// would mount the page once before its chunk loads and again after. -// AgentSidebar already lazy-loads its own panel. -import { AgentInspector } from "./AgentInspector"; -import { MobileAppBar } from "./MobileAppBar"; -import { Sidebar } from "./Sidebar"; - -const Header = lazy(() => - import("./Header").then((module) => ({ default: module.Header })), -); - interface LayoutProps { children: React.ReactNode; } -const SIDEBAR_COLLAPSE_KEY = "chat.sidebar.collapsed"; - -function routeOwnsToolbar(pathname: string): boolean { - return ( - pathname === HOME_PATH || - isChatPathname(pathname) || - pathname === "/database" || - pathname.startsWith("/extensions") - ); -} - /** - * A voice chat follows the user into a chat thread, so Get started can hand - * one off. Other pages show no controls for this session, so any other - * navigation ends it rather than leave the microphone open with nothing to - * stop it. The agent sidebar owns a separate session and keeps its own - * controls on screen, so this leaves that one alone. + * Clean Agent-Native app shell: a single full-height canvas with no chat or + * agent UI mounted by default. The app already satisfies the Agent-Native + * contract (data in SQL, actions as the source of truth, application state for + * navigation, real-time sync) — the agent rail and chat are opt-in surfaces. + * + * To add the persistent agent rail when the user asks for it, wrap {children} + * in `AgentSidebar` from "@agent-native/core/client/agent-chat" + * (position="right", a stable storageKey, browserTabId={TAB_ID} from + * "@/lib/tab-id"). See the `agent-native-toolkit` skill for the full pattern. */ -function EndVoiceChatOutsideChat() { - const { pathname } = useLocation(); - const end = useRealtimeVoiceModeOptional()?.end; - const previousPathnameRef = useRef(pathname); - - useEffect(() => { - if (previousPathnameRef.current === pathname) return; - previousPathnameRef.current = pathname; - if (!isChatPathname(pathname)) end?.(); - }, [end, pathname]); - - return null; -} - export function Layout({ children }: LayoutProps) { return ( - - - {children} - - ); -} - -function LayoutShell({ children }: LayoutProps) { - const location = useLocation(); - const t = useT(); - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const isChatRoute = isChatPathname(location.pathname); - // Settings brings its own navigation, header, and agent toggle, so it - // renders full width. - const settingsOwnsChrome = isSettingsPathname(location.pathname); - const chatHomeHandoffActive = useAgentChatHomeHandoff({ - storageKey: "chat", - activePath: location.pathname, - enabled: !isChatRoute, - }); - const chatHomeHandoffPending = isAgentChatHomeHandoffActive("chat"); - useAgentChatHomeHandoffLinks({ - storageKey: "chat", - isChatPath: isChatPathname, - requireActiveHandoff: true, - }); - - useEffect(() => { - setMobileSidebarOpen(false); - }, [location.pathname]); - - useEffect(() => { - const closeMobileSidebar = () => setMobileSidebarOpen(false); - window.addEventListener("agent-chat:open-thread", closeMobileSidebar); - return () => { - window.removeEventListener("agent-chat:open-thread", closeMobileSidebar); - }; - }, []); - - useEffect(() => { - try { - const stored = window.localStorage.getItem(SIDEBAR_COLLAPSE_KEY); - if (stored !== null) setSidebarCollapsed(stored === "1"); - } catch { - // Ignore storage access errors; the default collapsed state still works. - } - }, []); - - useEffect(() => { - try { - window.localStorage.setItem( - SIDEBAR_COLLAPSE_KEY, - sidebarCollapsed ? "1" : "0", - ); - } catch { - // Ignore storage access errors. - } - }, [sidebarCollapsed]); - - const pageTitle = useHeaderTitle(); - const pageActions = useHeaderActions(); - // Until the chat route publishes its title and toolbar here, the AgentKit - // header must stay visible on phones or the thread loses both. - const chatHeaderInAppBar = - isChatRoute && (pageTitle != null || pageActions != null); - const openMobileSidebar = () => setMobileSidebarOpen(true); - const ownsToolbar = routeOwnsToolbar(location.pathname); - const contentFrame = ( -
- {isChatRoute ? ( - - ) : ownsToolbar ? ( - - ) : settingsOwnsChrome ? null : ( - }> -
- - )} -
- {children} -
-
- ); - - return ( -
- {settingsOwnsChrome ? null : ( -
- -
- )} - - - - {t("navigation.navigation")} - - - {t("navigation.navigationDescription")} - - - - - {isChatRoute ? ( -
- {contentFrame} -
- ) : ( - - {contentFrame} - - )} +
+
+ {children} +
); }