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 { usePlugins } from "@/plugins"; import type { RegisteredPlugin } from "@/plugins"; 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) { 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; } export default function App() { const { t } = useI18n(); const { plugins } = usePlugins(); const navItems = useMemo( () => buildNavItems(BUILTIN_NAV, plugins), [plugins], ); return (
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", ) } > {({ isActive }) => ( <> {labelKey ? ((t.app.nav as Record)[ labelKey ] ?? label) : label} {isActive && ( )} )} ))}
{t.app.webUi}
} /> } /> } /> } /> } /> } /> } /> } /> {plugins.map(({ manifest, component: PluginComponent }) => ( } /> ))} } />
); } interface NavItem { icon: React.ComponentType<{ className?: string }>; label: string; labelKey?: string; path: string; }