/** * @file App.tsx * @description Top-level React tree for the AgentHub dashboard. * Wires together routing, real-time WebSocket ingestion, browser notifications, * and the splash screen shown on cold load. * * ## Data flow * 1. {@link useWebSocket} connects to the server's `/ws` endpoint. * 2. Each inbound {@link WSMessage} is published on the in-memory * {@link eventBus} so any page can subscribe without prop drilling. * 3. {@link useNotifications} listens for alert-worthy events and surfaces OS * notifications when permitted. * * ## Routing * All feature pages nest under {@link Layout}, which owns the sidebar and * passes `wsConnected` for the connection badge. Unknown paths fall through to * {@link NotFound}. * * @author Nguyễn Ngọc Trí Vĩ */ /* ============================================================================= * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed) * ============================================================================= * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode. * * ## Design constraints * - Local-first: no telemetry leaves the machine unless the user configures webhooks. * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that * philosophy by degrading gracefully (empty states, stale badges, reconnect loops). * - Destructive flows stay behind explicit confirmation modals and server-side gates. * - Internationalization: user-visible strings belong in i18n JSON, not literals here. * * ## Remote data & SSH * Remote Data Sources let operators aggregate multiple machines. SSH entries describe * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every * scoped GET via `?sources=`. Health checks and import history surface in Settings. * * ## Internal dependencies * - `./components/Layout` * - `./components/SplashScreen` * - `./pages/Dashboard` (lazy) * - `./pages/History` (lazy) * - `./pages/SessionDetail` (lazy) * - `./pages/SettingsHub` (lazy) * - `./pages/Workspace` (lazy) * * ## Testing pointers * - Prefer colocated `__tests__` with Vitest + Testing Library for UI. * - Server contract changes require `npm run test:server` and OpenAPI sync. * * ## Related docs * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline. * - `docs/API.md` — REST reference. * - `skills/file-headers/` — mandatory `@author` header policy. * ============================================================================= */ import { BrowserRouter, Routes, Route, Navigate, useParams, useLocation } from "react-router-dom"; import { lazy, Suspense, useCallback, useEffect, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Layout } from "./components/Layout"; import { DocumentTitle } from "./components/DocumentTitle"; import { SplashScreen } from "./components/SplashScreen"; import { GlobalTooltip } from "./components/Tooltip"; import { Toaster } from "./components/Toaster"; import { ConfirmHost } from "./components/ConfirmHost"; import { RouteErrorBoundary, reloadOnce } from "./components/RouteErrorBoundary"; import { usePermissions } from "./components/AuthGate"; import { NotFound } from "./pages/NotFound"; import { useWebSocket } from "./hooks/useWebSocket"; import { useNotifications } from "./hooks/useNotifications"; import { eventBus } from "./lib/eventBus"; import { copyOnCtrlShiftC } from "./lib/keys"; import { reloadOnNewBuild } from "./lib/newBuildReload"; import type { WSMessage } from "./lib/types"; // The heavy pages (xterm, the diff viewer, the settings editors) load on first // visit instead of in the initial bundle — the Dashboard and History too, so the // entry chunk holds only the shell. const Dashboard = lazy(() => import("./pages/Dashboard").then((m) => ({ default: m.Dashboard }))); const History = lazy(() => import("./pages/History").then((m) => ({ default: m.History }))); const Workspace = lazy(() => import("./pages/Workspace").then((m) => ({ default: m.Workspace }))); const SettingsHub = lazy(() => import("./pages/SettingsHub").then((m) => ({ default: m.SettingsHub })) ); const SessionDetail = lazy(() => import("./pages/SessionDetail").then((m) => ({ default: m.SessionDetail })) ); const SnapshotView = lazy(() => import("./pages/SnapshotView").then((m) => ({ default: m.SnapshotView })) ); const FilesPage = lazy(() => import("./pages/FilesPage").then((m) => ({ default: m.FilesPage }))); const Users = lazy(() => import("./pages/Users").then((m) => ({ default: m.Users }))); const Artifacts = lazy(() => import("./pages/Artifacts").then((m) => ({ default: m.Artifacts }))); function PageFallback() { const { t } = useTranslation(); return (
{t("common:loading")}
); } function Page({ children }: { children: ReactNode }) { const { pathname } = useLocation(); return ( }>{children} ); } /** A page only a shared server needs (the File Explorer): local mode goes home. */ function ServerOnly({ children }: { children: ReactNode }) { return usePermissions().local ? : <>{children}; } /** Keyed by the id so moving to another session starts from fresh state. */ function SessionDetailRoute() { const { id } = useParams(); return ; } /** * Application root component mounted by {@link main.tsx}. * @returns Routed dashboard UI inside `BrowserRouter`. */ export default function App() { const onMessage = useCallback((msg: WSMessage) => { eventBus.publish(msg); }, []); const { connected } = useWebSocket(onMessage); useNotifications(); // A self-update replaced the hashed assets this tab still references: Vite // reports the failed preload here; reload once (guarded against loops). useEffect(() => { const onPreloadError = (e: Event) => { if (reloadOnce()) e.preventDefault(); }; window.addEventListener("vite:preloadError", onPreloadError); return () => window.removeEventListener("vite:preloadError", onPreloadError); }, []); // The server came back as a newer version (any tab, any way it was updated): // load its UI build instead of leaving the user to press F5. useEffect(() => reloadOnNewBuild(), []); // Ctrl+Shift+C is copy here (Linux terminal habit), not the browser's inspector. useEffect(() => { window.addEventListener("keydown", copyOnCtrlShiftC, true); return () => window.removeEventListener("keydown", copyOnCtrlShiftC, true); }, []); return ( <> {/* One renderer gives every `title` in the app the designed bubble — see components/Tooltip.tsx for why it is not a wrapper component. */} {/* App-wide toasts and promise-based confirms: lib/toast.ts, lib/confirm.ts. */} }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); }