/**
* 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 (
{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).