/** * The CHAT-RAIL shell (guuey#303, founder-confirmed), on * `@guuey/agent-layout` (guuey#403 — this migration is the lib's * acceptance test): * * ┌──────────┬────────────────────────┐ * │ ☰ Menu 1 │ │ * │ ☰ Menu 2 │ MAIN CANVAS │ * │ ☰ Menu 3 │ your pages — or the │ * ├──────────┤ full generated UI │ * │ AGENT │ when the agent draws │ * │ RAIL │ one │ * │ (chat) │ │ * └──────────┴────────────────────────┘ * * Upper sidebar = your product's menus. LOWER sidebar = the embed-SDK * agent rail — the visitor TYPES THERE; generative views collapse to * compact CHIPS in the rail (the kit's chips presentation) and the full * render takes the MAIN canvas. Chips are the history: clicking one * re-selects its render onto the canvas ("just like a web browser's * history"); a new render navigates forward automatically; picking a * menu swaps the canvas back to your pages. * * The layout lib owns the AGENT-MODE physics — the two-tone sidebar, the * pane's ground following the user's attention (submit → agent tone; any * navigation → app tone, derived from the route change), the working * state while the agent has the room, light/dark via the SAME mode you * give the chat. The shell wires it once: `navigationKey` from the * router, `bindGuueyChat` onto the rail, `agentViewClosed` on the back * affordance. Zero per-link wiring. */ import { useCallback, useMemo, useRef, useState } from "react"; import type { GuueyChatHandle } from "@guuey/chat/react"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import type { PlanViewSummary, ViewRefItem } from "@guuey/chat"; import { GuueyView } from "@guuey/mcp-apps-host/react"; import { ActivePane, AgentModeProvider, AgentModeShell, AgentModeSidebar, SidebarPanel, bindGuueyChat, useAgentMode, } from "@guuey/agent-layout/react"; import { appConfig } from "../config"; import { AgentChat } from "../components/AgentChat"; import { currentIdentityMode, logOut } from "../lib/identity"; import { oidcConfigured, signOutOidc } from "../lib/oidc"; export function AppShell() { const location = useLocation(); return ( {appConfig.brand.logoText}} > ); } function ShellBody() { const navigate = useNavigate(); const mode = currentIdentityMode(); const { dispatch } = useAgentMode(); // The lib's chat bridge: submit/settle/view-mount flow into the // active-panel machine; the shell composes its own roster handling on top. const binding = useMemo(() => bindGuueyChat(dispatch), [dispatch]); // The rail↔canvas bridge: the kit's view roster (mount material lives // here, the rail shows only chips), the selected key, and whether the // canvas currently shows a view or the routed pages. const [views, setViews] = useState([]); // The chat handle carries the kit's view-host wiring (action relay, // model-context sink, theme announce) — the canvas mounts with it so a // rendered card's Confirm works OUTSIDE the transcript too (guuey#335). const [chat, setChat] = useState(null); const [selectedKey, setSelectedKey] = useState(undefined); const [canvasShowsView, setCanvasShowsView] = useState(false); const newestKeyRef = useRef(undefined); const onViewsChange = useCallback( (next: PlanViewSummary[]) => { binding.onViewsChange(next); // the (d) working state clears on a live mount setViews(next); // Browser-history forward-navigation: a NEW live render takes the // canvas. (Reversed find = newest mountable; live entries sit after // history in the roster's transcript order.) const newest = [...next].reverse().find((v) => v.mount !== null && v.phase !== "expired"); if (newest !== undefined && newest.key !== newestKeyRef.current) { newestKeyRef.current = newest.key; setSelectedKey(newest.key); setCanvasShowsView(true); // The demo-tour hook (guuey#303): step machines outside the app can // key on "the agent just drew UI". window.dispatchEvent( new CustomEvent("demo:render-complete", { detail: { key: newest.key, title: newest.title }, }), ); } }, [binding], ); const onViewRef = useCallback((item: ViewRefItem) => { setSelectedKey(item.key); setCanvasShowsView(true); }, []); const closeView = useCallback(() => { setCanvasShowsView(false); // The lib rule: closing an agent view returns the tone to the menu // UNLESS a turn is still streaming. dispatch({ type: "agentViewClosed" }); }, [dispatch]); const selected = views.find((v) => v.key === selectedKey && v.mount !== null); async function handleLogOut() { if (mode === "oidc" && oidcConfigured()) await signOutOidc(); logOut(); navigate("/"); } return (
{appConfig.brand.logoText} {appConfig.brand.name}
{canvasShowsView && selected !== undefined && selected.mount !== null ? (
{selected.title}
{selected.mount.channel !== "locator" ? ( ) : ( // A locator still resolving — the kit reads it and the next // roster emission carries the material (a FAILED read // surfaces as the chip's expired state instead).

Loading card…

)}
) : ( )}
); }