import { useMemo } from "react"; import { Routes, Route, NavLink, Navigate } from "react-router-dom"; import { Activity, BarChart3, Clock, FileText, KeyRound, MessageSquare, Package, Settings, Puzzle, Sparkles, Terminal, Globe, Database, Shield, Wrench, Zap, Heart, Star, Code, Eye, } from "lucide-react"; import { Cell, Grid, SelectionSwitcher, Typography } from "@nous-research/ui"; import { cn } from "@/lib/utils"; import { Backdrop } from "@/components/Backdrop"; import StatusPage from "@/pages/StatusPage"; import ConfigPage from "@/pages/ConfigPage"; import EnvPage from "@/pages/EnvPage"; import SessionsPage from "@/pages/SessionsPage"; import LogsPage from "@/pages/LogsPage"; import AnalyticsPage from "@/pages/AnalyticsPage"; import CronPage from "@/pages/CronPage"; import SkillsPage from "@/pages/SkillsPage"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { ThemeSwitcher } from "@/components/ThemeSwitcher"; import { useI18n } from "@/i18n"; import { PluginSlot, usePlugins } from "@/plugins"; import type { RegisteredPlugin } from "@/plugins"; import { useTheme } from "@/themes"; /** Built-in route → default page component. Used both for standard routing * and for resolving plugin `tab.override` values. Keys must match the * `path` in `BUILTIN_NAV` so `/path` lookups stay consistent. */ const BUILTIN_ROUTES: Record = { "/": StatusPage, "/sessions": SessionsPage, "/analytics": AnalyticsPage, "/logs": LogsPage, "/cron": CronPage, "/skills": SkillsPage, "/config": ConfigPage, "/env": EnvPage, }; const BUILTIN_NAV: NavItem[] = [ { path: "/", labelKey: "status", label: "Status", icon: Activity }, { path: "/sessions", labelKey: "sessions", label: "Sessions", icon: MessageSquare, }, { path: "/analytics", labelKey: "analytics", label: "Analytics", icon: BarChart3, }, { path: "/logs", labelKey: "logs", label: "Logs", icon: FileText }, { path: "/cron", labelKey: "cron", label: "Cron", icon: Clock }, { path: "/skills", labelKey: "skills", label: "Skills", icon: Package }, { path: "/config", labelKey: "config", label: "Config", icon: Settings }, { path: "/env", labelKey: "keys", label: "Keys", icon: KeyRound }, ]; // Plugins can reference any of these by name in their manifest — keeps bundle // size sane vs. importing the full lucide-react set. const ICON_MAP: Record> = { Activity, BarChart3, Clock, FileText, KeyRound, MessageSquare, Package, Settings, Puzzle, Sparkles, Terminal, Globe, Database, Shield, Wrench, Zap, Heart, Star, Code, Eye, }; function resolveIcon( name: string, ): React.ComponentType<{ className?: string }> { return ICON_MAP[name] ?? Puzzle; } function buildNavItems( builtIn: NavItem[], plugins: RegisteredPlugin[], ): NavItem[] { const items = [...builtIn]; for (const { manifest } of plugins) { // Plugins that replace a built-in route don't add a new tab entry — // they reuse the existing tab. The nav just lights up the original // built-in entry when the user visits `/`. if (manifest.tab.override) continue; // Hidden plugins register their component + slots but skip the nav. if (manifest.tab.hidden) continue; const pluginItem: NavItem = { path: manifest.tab.path, label: manifest.label, icon: resolveIcon(manifest.icon), }; const pos = manifest.tab.position ?? "end"; if (pos === "end") { items.push(pluginItem); } else if (pos.startsWith("after:")) { const target = "/" + pos.slice(6); const idx = items.findIndex((i) => i.path === target); items.splice(idx >= 0 ? idx + 1 : items.length, 0, pluginItem); } else if (pos.startsWith("before:")) { const target = "/" + pos.slice(7); const idx = items.findIndex((i) => i.path === target); items.splice(idx >= 0 ? idx : items.length, 0, pluginItem); } else { items.push(pluginItem); } } return items; } /** Build the final route table, letting plugins override built-in pages. * * Returns (path, Component, key) tuples. Plugins with `tab.override` * win over both built-ins and other plugins (last registration wins if * two plugins claim the same override, but we warn in dev). Plugins with * a regular `tab.path` register alongside built-ins as standalone * routes. */ function buildRoutes( plugins: RegisteredPlugin[], ): Array<{ key: string; path: string; Component: React.ComponentType }> { const overrides = new Map(); const addons: RegisteredPlugin[] = []; for (const p of plugins) { if (p.manifest.tab.override) { overrides.set(p.manifest.tab.override, p); } else { addons.push(p); } } const routes: Array<{ key: string; path: string; Component: React.ComponentType; }> = []; for (const [path, Component] of Object.entries(BUILTIN_ROUTES)) { const override = overrides.get(path); if (override) { routes.push({ key: `override:${override.manifest.name}`, path, Component: override.component, }); } else { routes.push({ key: `builtin:${path}`, path, Component }); } } for (const addon of addons) { // Don't double-register a plugin that shadows a built-in path via // `tab.path` — `override` is the supported mechanism for that. if (BUILTIN_ROUTES[addon.manifest.tab.path]) continue; routes.push({ key: `plugin:${addon.manifest.name}`, path: addon.manifest.tab.path, Component: addon.component, }); } return routes; } export default function App() { const { t } = useI18n(); const { plugins } = usePlugins(); const { theme } = useTheme(); const navItems = useMemo( () => buildNavItems(BUILTIN_NAV, plugins), [plugins], ); const routes = useMemo(() => buildRoutes(plugins), [plugins]); const layoutVariant = theme.layoutVariant ?? "standard"; const showSidebar = layoutVariant === "cockpit"; // Tiled layout drops the 1600px clamp so pages can use the full viewport; // standard + cockpit keep the centered reading width. const mainMaxWidth = layoutVariant === "tiled" ? "max-w-none" : "max-w-[1600px]"; return (
{/* Themes can style backdrop chrome via `componentStyles.backdrop.*` CSS vars read by . Plugins can also inject full components into the backdrop layer via the `backdrop` slot — useful for scanlines, parallax stars, hero artwork, etc. */}
Hermes
Agent
{navItems.map(({ path, label, labelKey, icon: Icon }) => ( cn( "group relative flex h-full w-full items-center gap-1.5", "px-2.5 sm:px-4 py-2", "font-mondwest text-[0.65rem] sm:text-[0.8rem] tracking-[0.12em]", "whitespace-nowrap transition-colors cursor-pointer", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground", isActive ? "text-midground" : "opacity-60 hover:opacity-100", ) } style={{ clipPath: "var(--component-tab-clip-path)", }} > {({ isActive }) => ( <> {labelKey ? ((t.app.nav as Record)[ labelKey ] ?? label) : label} {isActive && ( )} )} ))}
{t.app.webUi}
{/* Full-width banner slot under the nav, outside the main clamp — useful for marquee/alert/status strips themes want to show above page content. */}
{showSidebar && (
} /> )}
{routes.map(({ key, path, Component }) => ( } /> ))} } />
{t.app.footer.name} } /> {t.app.footer.org} } />
{/* Fixed-position overlay plugins (scanlines, vignettes, etc.) render above everything else. Each plugin is responsible for its own pointer-events and z-index. */} ); } interface NavItem { icon: React.ComponentType<{ className?: string }>; label: string; labelKey?: string; path: string; }