Files
hermes-agent/web/src/App.tsx

820 lines
24 KiB
TypeScript
Raw Normal View History

2026-04-22 23:25:17 -04:00
import {
useCallback,
useEffect,
useMemo,
useState,
type ComponentType,
type ReactNode,
} from "react";
2026-04-24 09:04:11 -04:00
import {
Routes,
Route,
NavLink,
Navigate,
useLocation,
useNavigate,
} from "react-router-dom";
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
import {
2026-04-19 15:21:57 -04:00
Activity,
BarChart3,
2026-04-24 08:22:44 -04:00
BookOpen,
2026-04-19 15:21:57 -04:00
Clock,
2026-04-22 23:25:17 -04:00
Code,
Cpu,
2026-04-22 23:25:17 -04:00
Database,
Download,
Eye,
2026-04-19 15:21:57 -04:00
FileText,
2026-04-22 23:25:17 -04:00
Globe,
Heart,
2026-04-19 15:21:57 -04:00
KeyRound,
2026-04-22 23:25:17 -04:00
Menu,
2026-04-19 15:21:57 -04:00
MessageSquare,
Package,
Puzzle,
2026-04-22 23:25:17 -04:00
RotateCw,
Settings,
Shield,
2026-04-19 15:21:57 -04:00
Sparkles,
2026-04-22 23:25:17 -04:00
Star,
2026-04-19 15:21:57 -04:00
Terminal,
Users,
2026-04-19 15:21:57 -04:00
Wrench,
2026-04-22 23:25:17 -04:00
X,
2026-04-19 15:21:57 -04:00
Zap,
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
} from "lucide-react";
import { Button } from "@nous-research/ui/ui/components/button";
import { ListItem } from "@nous-research/ui/ui/components/list-item";
import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-switcher";
import { Spinner } from "@nous-research/ui/ui/components/spinner";
import { Typography } from "@/components/NouiTypography";
2026-04-19 10:48:56 -04:00
import { cn } from "@/lib/utils";
import { Backdrop } from "@/components/Backdrop";
2026-04-22 23:25:17 -04:00
import { SidebarFooter } from "@/components/SidebarFooter";
import { SidebarStatusStrip } from "@/components/SidebarStatusStrip";
import { PageHeaderProvider } from "@/contexts/PageHeaderProvider";
import { useSystemActions } from "@/contexts/useSystemActions";
import type { SystemAction } from "@/contexts/system-actions-context";
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
import ConfigPage from "@/pages/ConfigPage";
2026-04-24 09:04:11 -04:00
import DocsPage from "@/pages/DocsPage";
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
import EnvPage from "@/pages/EnvPage";
import SessionsPage from "@/pages/SessionsPage";
import LogsPage from "@/pages/LogsPage";
import AnalyticsPage from "@/pages/AnalyticsPage";
import ModelsPage from "@/pages/ModelsPage";
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
import CronPage from "@/pages/CronPage";
import ProfilesPage from "@/pages/ProfilesPage";
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
import SkillsPage from "@/pages/SkillsPage";
import PluginsPage from "@/pages/PluginsPage";
feat(web): add /api/pty WebSocket bridge to embed TUI in dashboard Exposes hermes --tui over a PTY-backed WebSocket so the dashboard can embed the real TUI rather than reimplement its surface. The browser attaches xterm.js to the socket; keystrokes flow in, PTY output bytes flow out. Architecture: browser <Terminal> (xterm.js) │ onData ───► ws.send(keystrokes) │ onResize ► ws.send('\x1b[RESIZE:cols;rows]') │ write ◄── ws.onmessage (PTY bytes) ▼ FastAPI /api/pty (token-gated, loopback-only) ▼ PtyBridge (ptyprocess) ── spawns node ui-tui/dist/entry.js ──► tui_gateway + AIAgent Components ---------- hermes_cli/pty_bridge.py Thin wrapper around ptyprocess.PtyProcess: byte-safe read/write on the master fd via os.read/os.write (not PtyProcessUnicode — ANSI is inherently byte-oriented and UTF-8 boundaries may land mid-read), non-blocking select-based reads, TIOCSWINSZ resize, idempotent SIGHUP→SIGTERM→SIGKILL teardown, platform guard (POSIX-only; Windows is WSL-supported only). hermes_cli/web_server.py @app.websocket("/api/pty") endpoint gated by the existing _SESSION_TOKEN (via ?token= query param since browsers can't set Authorization on WS upgrades). Loopback-only enforcement. Reader task uses run_in_executor to pump PTY bytes without blocking the event loop. Writer loop intercepts a custom \x1b[RESIZE:cols;rows] escape before forwarding to the PTY. The endpoint resolves the TUI argv through a _resolve_chat_argv hook so tests can inject fake commands without building the real TUI. Tests ----- tests/hermes_cli/test_pty_bridge.py — 12 unit tests: spawn, stdout, stdin round-trip, EOF, resize (via TIOCSWINSZ + tput readback), close idempotency, cwd, env forwarding, unavailable-platform error. tests/hermes_cli/test_web_server.py — TestPtyWebSocket adds 7 tests: missing/bad token rejection (close code 4401), stdout streaming, stdin round-trip, resize escape forwarding, unavailable-platform ANSI error frame + 1011 close, resume parameter forwarding to argv. 96 tests pass under scripts/run_tests.sh. (cherry picked from commit 29b337bca70fc9efb082a5a852ea2cd5381af1a9) feat(web): add Chat tab with xterm.js terminal + Sessions resume button (cherry picked from commit 3d21aee8 by emozilla, conflicts resolved against current main: BUILTIN_ROUTES table + plugin slot layout) fix(tui): replace OSC 52 jargon in /copy confirmation When the user ran /copy successfully, Ink confirmed with: sent OSC52 copy sequence (terminal support required) That reads like a protocol spec to everyone who isn't a terminal implementer. The caveat was a historical artifact — OSC 52 wasn't universally supported when this message was written, so the TUI honestly couldn't guarantee the copy had landed anywhere. Today every modern terminal (including the dashboard's embedded xterm.js) handles OSC 52 reliably. Say what the user actually wants to know — that it copied, and how much — matching the message the TUI already uses for selection copy: copied 1482 chars (cherry picked from commit a0701b1d5a598dd1d3b94038a7bcbb2a3ab559fc) docs: document the dashboard Chat tab AGENTS.md — new subsection under TUI Architecture explaining that the dashboard embeds the real hermes --tui rather than rewriting it, with pointers to the pty_bridge + WebSocket endpoint and the rule 'never add a parallel chat surface in React.' website/docs/user-guide/features/web-dashboard.md — user-facing Chat section inside the existing Web Dashboard page, covering how it works (WebSocket + PTY + xterm.js), the Sessions-page resume flow, and prerequisites (Node.js, ptyprocess, POSIX kernel / WSL on Windows). (cherry picked from commit 2c2e32cc4519973c77b63016316b065c0f656704) feat(tui-gateway): transport-aware dispatch + WebSocket sidecar Decouples the JSON-RPC dispatcher from its I/O sink so the same handler surface can drive multiple transports concurrently. The PTY chat tab already speaks to the TUI binary as bytes — this adds a structured event channel alongside it for dashboard-side React widgets that need typed events (tool.start/complete, model picker state, slash catalog) that PTY can't surface. - `tui_gateway/transport.py` — `Transport` protocol + `contextvars` binding + module-level `StdioTransport` fallback. The stdio stream resolves through a lambda so existing tests that monkey-patch `_real_stdout` keep passing without modification. - `tui_gateway/ws.py` — WebSocket transport implementation; FastAPI endpoint mounting lives in hermes_cli/web_server.py. - `tui_gateway/server.py`: - `write_json` routes via session transport (for async events) → contextvar transport (for in-request writes) → stdio fallback. - `dispatch(req, transport=None)` binds the transport for the request lifetime and propagates it to pool workers via `contextvars.copy_context` so async handlers don't lose their sink. - `_init_session` and the manual-session create path stash the request's transport so out-of-band events (subagent.complete, etc.) fan out to the right peer. `tui_gateway.entry` (Ink's stdio handshake) is unchanged externally — it falls through every precedence step into the stdio fallback, byte- identical to the previous behaviour. feat(web): ChatSidebar — JSON-RPC sidecar next to xterm.js terminal Composes the two transports into a single Chat tab: ┌─────────────────────────────────────────┬──────────────┐ │ xterm.js / PTY (emozilla #13379) │ ChatSidebar │ │ the literal hermes --tui process │ /api/ws │ └─────────────────────────────────────────┴──────────────┘ terminal bytes structured events The terminal pane stays the canonical chat surface — full TUI fidelity, slash commands, model picker, mouse, skin engine, wide chars all paint inside the terminal. The sidebar opens a parallel JSON-RPC WebSocket to the same gateway and renders metadata that PTY can't surface to React chrome: • model + provider badge with connection state (click → switch) • running tool-call list (driven by tool.start / tool.progress / tool.complete events) • model picker dialog (gateway-driven, reuses ModelPickerDialog) The sidecar is best-effort. If the WS can't connect (older gateway, network hiccup, missing token) the terminal pane keeps working unimpaired — sidebar just shows the connection-state badge in the appropriate tone. - `web/src/components/ChatSidebar.tsx` — new component (~270 lines). Owns its GatewayClient, drives the model picker through `slash.exec`, fans tool events into a capped tool list. - `web/src/pages/ChatPage.tsx` — split layout: terminal pane (`flex-1`) + sidebar (`w-80`, `lg+` only). - `hermes_cli/web_server.py` — mount `/api/ws` (token + loopback guards mirror /api/pty), delegate to `tui_gateway.ws.handle_ws`. Co-authored-by: emozilla <emozilla@nousresearch.com> refactor(web): /clean pass on ChatSidebar + ChatPage lint debt - ChatSidebar: lift gw out of useRef into a useMemo derived from a reconnect counter. React 19's react-hooks/refs and react-hooks/ set-state-in-effect rules both fire when you touch a ref during render or call setState from inside a useEffect body. The counter-derived gw is the canonical pattern for "external resource that needs to be replaceable on user action" — re-creating the client comes from bumping `version`, the effect just wires + tears down. Drops the imperative `gwRef.current = …` reassign in reconnect, drops the truthy ref guard in JSX. modelLabel + banner inlined as derived locals (one-off useMemo was overkill). - ChatPage: lazy-init the banner state from the missing-token check so the effect body doesn't have to setState on first run. Drops the unused react-hooks/exhaustive-deps eslint-disable. Adds a scoped no-control-regex disable on the SGR mouse parser regex (the \\x1b is intentional for xterm escape sequences). All my-touched files now lint clean. Remaining warnings on web/ belong to pre-existing files this PR doesn't touch. Verified: vitest 249/249, ui-tui eslint clean, web tsc clean, python imports clean. chore: uptick fix(web): drop ChatSidebar tool list — events can't cross PTY/WS boundary The /api/pty endpoint spawns `hermes --tui` as a child process with its own tui_gateway and _sessions dict; /api/ws runs handle_ws in-process in the dashboard server with a separate _sessions dict. Tool events fire on the child's gateway and never reach the WS sidecar, so the sidebar's tool.start/progress/complete listeners always observed an empty list. Drop the misleading list (and the now-orphaned ToolCall primitive), keep model badge + connection state + model picker + error banner — those work because they're sidecar-local concerns. Surfacing tool calls in the sidebar requires cross-process forwarding (PTY child opens a back-WS to the dashboard, gateway tees emits onto stdio + sidecar transport) — proper feature for a follow-up. feat(web): wire ChatSidebar tool list to PTY child via /api/pub broadcast The dashboard's /api/pty spawns hermes --tui as a child process; tool events fire in the python tui_gateway grandchild and never crossed the process boundary into the in-process WS sidecar — so the sidebar tool list was always empty. Cross-process forwarding: - tui_gateway: TeeTransport (transport.py) + WsPublisherTransport (event_publisher.py, sync websockets client). entry.py installs the tee on _stdio_transport when HERMES_TUI_SIDECAR_URL is set, mirroring every dispatcher emit to a back-WS without disturbing Ink's stdio handshake. - hermes_cli/web_server.py: new /api/pub (publisher) + /api/events (subscriber) endpoints with a per-channel registry. /api/pty now accepts ?channel= and propagates the sidecar URL via env. start_server also stashes app.state.bound_port so the URL is constructable. - web/src/pages/ChatPage.tsx: generates a channel UUID per mount, passes it to /api/pty and as a prop to ChatSidebar. - web/src/components/ChatSidebar.tsx: opens /api/events?channel=, fans tool.start/progress/complete back into the ToolCall list. Restores the ToolCall primitive. Tests: 4 new TestPtyWebSocket cases cover channel propagation, broadcast fan-out, and missing-channel rejection (10 PTY tests pass, 120 web_server tests overall). fix(web): address Copilot review on #14890 Five threads, all real: - gatewayClient.ts: register `message`/`close` listeners BEFORE awaiting the open handshake. Server emits `gateway.ready` immediately after accept, so a listener attached after the open promise could race past the initial skin payload and lose it. - ChatSidebar.tsx: wire `error`/`close` on the /api/events subscriber WS into the existing error banner. 4401/4403 (auth/loopback reject) surface as a "reload the page" message; mid-stream drops surface as "events feed disconnected" with the existing reconnect button. Clean unmount closes (1000/1001) stay silent. - web-dashboard.md: install hint was `pip install hermes-agent[web]` but ptyprocess lives in the `pty` extra, not `web`. Switch to `hermes-agent[web,pty]` in both prerequisite blocks. - AGENTS.md: previous "never add a parallel React chat surface" guidance was overbroad and contradicted this PR's sidebar. Tightened to forbid re-implementing the transcript/composer/PTY terminal while explicitly allowing structured supporting widgets (sidebar / model picker / inspectors), matching the actual architecture. - web/package-lock.json: regenerated cleanly so the wterm sibling workspace paths (extraneous machine-local entries) stop polluting CI. Tests: 249/249 vitest, 10/10 PTY/events, web tsc clean. refactor(web): /clean pass on ChatSidebar events handler Spotted in the round-2 review: - Banner flashed on clean unmount: `ws.close()` from the effect cleanup fires `close` with code 1005, opened=true, neither 1000 nor 1001 — hit the "unexpected drop" branch. Track `unmounting` in the effect scope and gate the banner through a `surface()` helper so cleanup closes stay silent. - DRY the duplicated "events feed disconnected" string into a local const used by both the error and close handlers. - Drop the `opened` flag (no longer needed once the unmount guard is the source of truth for "is this an expected close?").
2026-04-21 02:09:12 -04:00
import ChatPage from "@/pages/ChatPage";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { ThemeSwitcher } from "@/components/ThemeSwitcher";
import { useI18n } from "@/i18n";
import type { Translations } from "@/i18n/types";
2026-04-22 23:25:17 -04:00
import { PluginPage, PluginSlot, usePlugins } from "@/plugins";
import type { PluginManifest } from "@/plugins";
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
import { useTheme } from "@/themes";
2026-04-24 12:07:46 -04:00
import { isDashboardEmbeddedChatEnabled } from "@/lib/dashboard-flags";
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
2026-04-22 23:25:17 -04:00
function RootRedirect() {
return <Navigate to="/sessions" replace />;
}
function UnknownRouteFallback({ pluginsLoading }: { pluginsLoading: boolean }) {
if (pluginsLoading) {
// Render nothing during the plugin-load window — a spinner here would just flash.
return null;
}
return <Navigate to="/sessions" replace />;
}
2026-04-24 12:07:46 -04:00
const CHAT_NAV_ITEM: NavItem = {
path: "/chat",
labelKey: "chat",
label: "Chat",
icon: Terminal,
};
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
/**
* Built-in routes except /chat. Chat is rendered persistently (outside
* <Routes>) when embedded see the persistent chat host block rendered
* inline near the bottom of this file so the PTY child, WebSocket,
* and xterm instance survive when the user visits another tab and comes
* back. A `display:none` toggle hides the terminal without unmounting.
* Routing still owns the URL so /chat deep-links, browser back/forward,
* and nav highlight keep working.
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
*/
2026-04-24 12:07:46 -04:00
const BUILTIN_ROUTES_CORE: Record<string, ComponentType> = {
2026-04-22 23:25:17 -04:00
"/": RootRedirect,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
"/sessions": SessionsPage,
"/analytics": AnalyticsPage,
"/models": ModelsPage,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
"/logs": LogsPage,
"/cron": CronPage,
"/skills": SkillsPage,
"/plugins": PluginsPage,
"/profiles": ProfilesPage,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
"/config": ConfigPage,
"/env": EnvPage,
2026-04-24 09:04:11 -04:00
"/docs": DocsPage,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
};
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
// Route placeholder for /chat. The persistent ChatPage host (rendered
// outside <Routes> when embedded chat is on) paints on top; this empty
// element just claims the path so the `*` catch-all redirect doesn't
// fire when the user navigates to /chat.
function ChatRouteSink() {
return null;
}
2026-04-24 12:07:46 -04:00
const BUILTIN_NAV_REST: NavItem[] = [
2026-04-19 15:21:57 -04:00
{
path: "/sessions",
labelKey: "sessions",
label: "Sessions",
icon: MessageSquare,
},
{
path: "/analytics",
labelKey: "analytics",
label: "Analytics",
icon: BarChart3,
},
{
path: "/models",
labelKey: "models",
label: "Models",
icon: Cpu,
},
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
{ 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: "/plugins", labelKey: "plugins", label: "Plugins", icon: Puzzle },
{ path: "/profiles", labelKey: "profiles", label: "Profiles", icon: Users },
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
{ path: "/config", labelKey: "config", label: "Config", icon: Settings },
{ path: "/env", labelKey: "keys", label: "Keys", icon: KeyRound },
2026-04-24 08:22:44 -04:00
{
2026-04-24 09:04:11 -04:00
path: "/docs",
2026-04-24 08:22:44 -04:00
labelKey: "documentation",
label: "Documentation",
icon: BookOpen,
},
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
];
2026-04-22 23:25:17 -04:00
const ICON_MAP: Record<string, ComponentType<{ className?: string }>> = {
2026-04-19 15:21:57 -04:00
Activity,
BarChart3,
Clock,
Cpu,
2026-04-19 15:21:57 -04:00
FileText,
KeyRound,
MessageSquare,
Package,
Settings,
Puzzle,
Sparkles,
Terminal,
Globe,
Database,
Shield,
Users,
2026-04-19 15:21:57 -04:00
Wrench,
Zap,
Heart,
Star,
Code,
Eye,
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
};
2026-04-22 23:25:17 -04:00
function resolveIcon(name: string): ComponentType<{ className?: string }> {
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
return ICON_MAP[name] ?? Puzzle;
}
function buildNavItems(
builtIn: NavItem[],
manifests: PluginManifest[],
): NavItem[] {
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
const items = [...builtIn];
2026-04-22 23:25:17 -04:00
for (const manifest of manifests) {
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
if (manifest.tab.override) continue;
if (manifest.tab.hidden) continue;
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
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;
}
/** Split merged nav into built-in sidebar entries vs plugin tabs, preserving plugin order hints. */
function partitionSidebarNav(
builtIn: NavItem[],
manifests: PluginManifest[],
): { coreItems: NavItem[]; pluginItems: NavItem[] } {
const merged = buildNavItems(builtIn, manifests);
const builtinPaths = new Set(builtIn.map((i) => i.path));
const coreItems: NavItem[] = [];
const pluginItems: NavItem[] = [];
for (const item of merged) {
if (builtinPaths.has(item.path)) coreItems.push(item);
else pluginItems.push(item);
}
return { coreItems, pluginItems };
}
2026-04-24 12:07:46 -04:00
function buildRoutes(
builtinRoutes: Record<string, ComponentType>,
manifests: PluginManifest[],
): Array<{
2026-04-22 23:25:17 -04:00
key: string;
path: string;
element: ReactNode;
}> {
const byOverride = new Map<string, PluginManifest>();
const addons: PluginManifest[] = [];
for (const m of manifests) {
if (m.tab.override) {
byOverride.set(m.tab.override, m);
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
} else {
2026-04-22 23:25:17 -04:00
addons.push(m);
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
}
}
const routes: Array<{
key: string;
path: string;
2026-04-22 23:25:17 -04:00
element: ReactNode;
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
}> = [];
2026-04-24 12:07:46 -04:00
for (const [path, Component] of Object.entries(builtinRoutes)) {
2026-04-22 23:25:17 -04:00
const om = byOverride.get(path);
if (om) {
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
routes.push({
2026-04-22 23:25:17 -04:00
key: `override:${om.name}`,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
path,
2026-04-22 23:25:17 -04:00
element: <PluginPage name={om.name} />,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
});
} else {
2026-04-22 23:25:17 -04:00
routes.push({ key: `builtin:${path}`, path, element: <Component /> });
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
}
}
2026-04-22 23:25:17 -04:00
for (const m of addons) {
if (m.tab.hidden) continue;
if (m.tab.path === "/plugins") continue;
2026-04-24 12:07:46 -04:00
if (builtinRoutes[m.tab.path]) continue;
2026-04-22 23:25:17 -04:00
routes.push({
key: `plugin:${m.name}`,
path: m.tab.path,
element: <PluginPage name={m.name} />,
});
}
for (const m of manifests) {
if (!m.tab.hidden) continue;
if (m.tab.path === "/plugins") continue;
2026-04-24 12:07:46 -04:00
if (builtinRoutes[m.tab.path] || m.tab.override) continue;
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
routes.push({
2026-04-22 23:25:17 -04:00
key: `plugin:hidden:${m.name}`,
path: m.tab.path,
element: <PluginPage name={m.name} />,
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
});
}
return routes;
}
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
export default function App() {
const { t } = useI18n();
2026-04-24 09:04:11 -04:00
const { pathname } = useLocation();
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
const { manifests, loading: pluginsLoading } = usePlugins();
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
const { theme } = useTheme();
2026-04-22 23:25:17 -04:00
const [mobileOpen, setMobileOpen] = useState(false);
const closeMobile = useCallback(() => setMobileOpen(false), []);
2026-04-24 09:04:11 -04:00
const isDocsRoute = pathname === "/docs" || pathname === "/docs/";
2026-04-24 12:07:46 -04:00
const normalizedPath = pathname.replace(/\/$/, "") || "/";
const isChatRoute = normalizedPath === "/chat";
const embeddedChat = isDashboardEmbeddedChatEnabled();
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
// A plugin can replace the built-in /chat page via `tab.override: "/chat"`
// in its manifest. When one does, `buildRoutes` already swaps the route
// element for <PluginPage /> — but we also have to suppress the
// persistent ChatPage host below, or the plugin's page and the built-in
// terminal would paint on top of each other. The override is niche
// (nothing ships overriding /chat today) but it's an advertised
// extension point, so preserve the pre-persistence contract: when a
// plugin owns /chat, the built-in chat UI is entirely absent.
//
// Waiting on `pluginsLoading` is load-bearing: manifests arrive
// asynchronously from /api/dashboard/plugins, so on initial render
// `chatOverriddenByPlugin` is always false. Without the loading
// gate, the persistent host would mount, spawn a PTY, and THEN get
// yanked out from under the user when the plugin's manifest resolves
// — killing the session mid-paint. Delaying host mount by the
// plugin-load window (typically <50ms, worst case 2s safety timeout)
// is the cheaper trade-off.
const chatOverriddenByPlugin = useMemo(
() => manifests.some((m) => m.tab.override === "/chat"),
[manifests],
);
2026-04-24 12:07:46 -04:00
const builtinRoutes = useMemo(
() => ({
...BUILTIN_ROUTES_CORE,
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
...(embeddedChat ? { "/chat": ChatRouteSink } : {}),
2026-04-24 12:07:46 -04:00
}),
[embeddedChat],
);
const builtinNav = useMemo(
() =>
embeddedChat ? [CHAT_NAV_ITEM, ...BUILTIN_NAV_REST] : BUILTIN_NAV_REST,
[embeddedChat],
);
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
const sidebarNav = useMemo(
() => partitionSidebarNav(builtinNav, manifests),
2026-04-24 12:07:46 -04:00
[builtinNav, manifests],
);
const routes = useMemo(
() => buildRoutes(builtinRoutes, manifests),
[builtinRoutes, manifests],
2026-04-22 23:25:17 -04:00
);
const pluginTabMeta = useMemo(
() =>
manifests
.filter((m) => !m.tab.hidden)
.map((m) => ({
path: m.tab.override ?? m.tab.path,
label: m.label,
})),
[manifests],
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
);
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
const layoutVariant = theme.layoutVariant ?? "standard";
2026-04-22 23:25:17 -04:00
useEffect(() => {
if (!mobileOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setMobileOpen(false);
};
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
};
}, [mobileOpen]);
useEffect(() => {
const mql = window.matchMedia("(min-width: 1024px)");
const onChange = (e: MediaQueryListEvent) => {
if (e.matches) setMobileOpen(false);
};
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, []);
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
return (
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
<div
data-layout-variant={layoutVariant}
2026-04-22 23:25:17 -04:00
className="font-mondwest flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden bg-black uppercase text-midground antialiased"
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
>
2026-04-19 10:48:56 -04:00
<SelectionSwitcher />
<Backdrop />
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
<PluginSlot name="backdrop" />
2026-04-19 10:48:56 -04:00
<header
className={cn(
2026-04-22 23:25:17 -04:00
"lg:hidden fixed top-0 left-0 right-0 z-40 h-12",
"flex items-center gap-2 px-3",
2026-04-19 10:48:56 -04:00
"border-b border-current/20",
"bg-background-base/90 backdrop-blur-sm",
)}
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
style={{
background: "var(--component-header-background)",
borderImage: "var(--component-header-border-image)",
clipPath: "var(--component-header-clip-path)",
}}
2026-04-19 10:48:56 -04:00
>
2026-04-28 12:02:34 -04:00
<Button
ghost
size="icon"
2026-04-22 23:25:17 -04:00
onClick={() => setMobileOpen(true)}
aria-label={t.app.openNavigation}
aria-expanded={mobileOpen}
aria-controls="app-sidebar"
2026-04-28 12:02:34 -04:00
className="text-midground/70 hover:text-midground"
2026-04-22 23:25:17 -04:00
>
2026-04-28 12:02:34 -04:00
<Menu />
</Button>
2026-04-22 23:25:17 -04:00
<Typography
className="font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
{t.app.brand}
</Typography>
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
</header>
2026-04-22 23:25:17 -04:00
{mobileOpen && (
<Button
ghost
2026-04-22 23:25:17 -04:00
aria-label={t.app.closeNavigation}
onClick={closeMobile}
className={cn(
"lg:hidden fixed inset-0 z-40 p-0 block",
"bg-black/60 backdrop-blur-sm",
2026-04-22 23:25:17 -04:00
)}
/>
)}
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
<PluginSlot name="header-banner" />
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
2026-04-22 23:25:17 -04:00
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden pt-12 lg:pt-0">
<div className="flex min-h-0 min-w-0 flex-1">
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
<aside
2026-04-22 23:25:17 -04:00
id="app-sidebar"
aria-label={t.app.navigation}
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
className={cn(
2026-04-22 23:25:17 -04:00
"fixed top-0 left-0 z-50 flex h-dvh max-h-dvh w-64 min-h-0 flex-col",
"border-r border-current/20",
"bg-background-base/95 backdrop-blur-sm",
"transition-transform duration-200 ease-out",
mobileOpen ? "translate-x-0" : "-translate-x-full",
"lg:sticky lg:top-0 lg:translate-x-0 lg:shrink-0",
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
)}
style={{
background: "var(--component-sidebar-background)",
clipPath: "var(--component-sidebar-clip-path)",
borderImage: "var(--component-sidebar-border-image)",
}}
>
2026-04-22 23:25:17 -04:00
<div
className={cn(
"flex h-14 shrink-0 items-center justify-between gap-2",
2026-04-22 23:25:17 -04:00
"border-b border-current/20",
)}
>
<div className="flex items-center gap-2">
<PluginSlot name="header-left" />
<Typography
className="font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
style={{ mixBlendMode: "plus-lighter" }}
>
Hermes
<br />
Agent
</Typography>
</div>
2026-04-22 23:25:17 -04:00
2026-04-28 12:02:34 -04:00
<Button
ghost
size="icon"
2026-04-22 23:25:17 -04:00
onClick={closeMobile}
aria-label={t.app.closeNavigation}
2026-04-28 12:02:34 -04:00
className="lg:hidden text-midground/70 hover:text-midground"
2026-04-22 23:25:17 -04:00
>
2026-04-28 12:02:34 -04:00
<X />
</Button>
2026-04-22 23:25:17 -04:00
</div>
<nav
2026-04-24 01:04:19 -04:00
className="min-h-0 w-full flex-1 overflow-y-auto overflow-x-hidden border-t border-current/10 py-2"
2026-04-22 23:25:17 -04:00
aria-label={t.app.navigation}
>
<ul className="flex flex-col">
{sidebarNav.coreItems.map((item) => (
<SidebarNavLink
closeMobile={closeMobile}
item={item}
key={item.path}
t={t}
/>
))}
2026-04-22 23:25:17 -04:00
</ul>
{sidebarNav.pluginItems.length > 0 && (
<div
aria-labelledby="hermes-sidebar-plugin-nav-heading"
className="flex flex-col border-t border-current/10 pb-2"
role="group"
>
<span
className={cn(
"px-5 pt-2.5 pb-1",
"font-mondwest text-[0.6rem] tracking-[0.15em] uppercase opacity-30",
)}
id="hermes-sidebar-plugin-nav-heading"
>
{t.app.pluginNavSection}
</span>
<ul className="flex flex-col">
{sidebarNav.pluginItems.map((item) => (
<SidebarNavLink
closeMobile={closeMobile}
item={item}
key={item.path}
t={t}
/>
))}
</ul>
</div>
)}
2026-04-22 23:25:17 -04:00
</nav>
<SidebarSystemActions onNavigate={closeMobile} />
<div
className={cn(
"flex shrink-0 items-center justify-between gap-2",
"px-3 py-2",
"border-t border-current/20",
)}
>
<div className="flex min-w-0 items-center gap-2">
<PluginSlot name="header-right" />
<ThemeSwitcher dropUp />
<LanguageSwitcher />
</div>
</div>
<SidebarFooter />
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
</aside>
feat: dashboard plugin system — extend the web UI with custom tabs Add a plugin system that lets plugins add new tabs to the dashboard. Plugins live in ~/.hermes/plugins/<name>/dashboard/ alongside any existing CLI/gateway plugin code. Plugin structure: plugins/<name>/dashboard/ manifest.json # name, label, icon, tab config, entry point dist/index.js # pre-built JS bundle (IIFE, uses SDK globals) plugin_api.py # optional FastAPI router mounted at /api/plugins/<name>/ Backend (hermes_cli/web_server.py): - Plugin discovery: scans plugins/*/dashboard/manifest.json from user, bundled, and project plugin directories - GET /api/dashboard/plugins — returns discovered plugin manifests - GET /api/dashboard/plugins/rescan — force re-discovery - GET /dashboard-plugins/<name>/<path> — serves plugin static assets with path traversal protection - Optional API route mounting: imports plugin_api.py and mounts its router under /api/plugins/<name>/ - Plugin API routes bypass session token auth (localhost-only) Frontend (web/src/plugins/): - Plugin SDK exposed on window.__HERMES_PLUGIN_SDK__ — provides React, hooks, UI components (Card, Badge, Button, etc.), API client, fetchJSON, theme/i18n hooks, and utilities - Plugin registry on window.__HERMES_PLUGINS__.register(name, Component) - usePlugins() hook: fetches manifests, loads JS/CSS, resolves components - App.tsx dynamically adds nav items and routes for discovered plugins - Icon resolution via static map of 20 common Lucide icons (no tree- shaking penalty — bundle only +5KB over baseline) Example plugin (plugins/example-dashboard/): - Demonstrates SDK usage: Card components, backend API call, SDK reference - Backend route: GET /api/plugins/example/hello Tested: plugin discovery, static serving, API routes, path traversal blocking, unknown plugin 404, bundle size (400KB vs 394KB baseline).
2026-04-16 03:10:28 -07:00
2026-04-22 23:25:17 -04:00
<PageHeaderProvider pluginTabs={pluginTabMeta}>
2026-04-24 10:17:57 -04:00
<div
2026-04-22 23:25:17 -04:00
className={cn(
2026-04-24 09:04:11 -04:00
"relative z-2 flex min-w-0 min-h-0 flex-1 flex-col",
2026-04-24 10:17:57 -04:00
"px-3 sm:px-6",
2026-04-24 12:07:46 -04:00
isChatRoute
? "pb-3 pt-1 sm:pb-4 sm:pt-2 lg:pt-4"
: "pt-2 sm:pt-4 lg:pt-6 pb-4 sm:pb-8",
2026-04-24 10:17:57 -04:00
isDocsRoute && "min-h-0 flex-1",
2026-04-22 23:25:17 -04:00
)}
>
<PluginSlot name="pre-main" />
2026-04-24 09:04:11 -04:00
<div
className={cn(
2026-04-24 10:17:57 -04:00
"w-full min-w-0",
(isDocsRoute || isChatRoute) &&
"min-h-0 flex flex-1 flex-col",
2026-04-24 09:04:11 -04:00
)}
>
2026-04-22 23:25:17 -04:00
<Routes>
{routes.map(({ key, path, element }) => (
<Route key={key} path={path} element={element} />
))}
<Route
path="*"
element={
<UnknownRouteFallback pluginsLoading={pluginsLoading} />
}
2026-04-22 23:25:17 -04:00
/>
</Routes>
fix(dashboard): persist chat tab state across tab switches The dashboard's Chat tab (hermes dashboard --tui) lost its session whenever the user navigated to another tab and came back. React Router unmounted ChatPage on path change, which ran the cleanup function, closed the PTY WebSocket, and terminated the underlying TUI child - so the next mount generated a fresh channel id, spawned a new PTY, and started a brand-new conversation. Rather than rebuild the destroyed state (session id capture + resume via HERMES_TUI_RESUME would reload history from disk but drop in-flight tool state, scrollback, and picker position), keep the component tree alive. * Pull ChatPage out of Routes into a sibling always-mounted host that toggles visibility via display:none keyed off the current route. A tiny ChatRouteSink still claims /chat so the catch-all redirect does not fire. * xterm instance, WebSocket, PTY child, and TUI/agent state all survive; returning to /chat shows the exact conversation the user left. * Respect plugin `/chat` overrides: if a plugin manifest declares `tab.override: "/chat"`, the Routes tree already swaps the element for <PluginPage /> — we additionally suppress the persistent host so the two don't paint on top of each other. Preserves the pre-persistence contract that a plugin owning /chat replaces the built-in chat UI entirely. * Wait for usePlugins() to finish loading before mounting the persistent host. Manifests arrive asynchronously from /api/dashboard/plugins, so without the `!pluginsLoading` gate the host would mount with manifests=[], spawn a PTY, and then unmount mid-session when the manifest list resolves and reveals a /chat override. Typical delay is <50ms; worst case is the 2s plugin- registration safety timeout. Cheaper than killing someone's conversation underneath them. * Gate page-header slot (`setEnd`), the mobile sheet's portalled render, and body-scroll lock on a new `isActive` prop so the hidden ChatPage doesn't fight the active page for shared state. The scroll-lock effect keys on the *derived* `mobilePanelOpen` (which is `isActive && mobilePanelOpenRaw`) rather than the raw state — that way tab-switch flips the dep false, fires the cleanup, and releases `document.body.style.overflow`. Keying on the raw state would leave body.overflow="hidden" stuck on /sessions and every other tab until the user navigated back to /chat and explicitly closed the sheet. * When isActive flips false to true, force a double-rAF fit: display:none collapses the host box and ResizeObserver does not fire on display changes, so xterm would otherwise stay at a stale or 1x1 grid. Also early-return from syncTerminalMetrics when the host has zero area, since fit() on a zero-sized element produces a 1x1 terminal. * Focus handling on tab return: only steal focus into the terminal if focus wasn't already parked somewhere inside ChatPage (e.g. the sidebar model picker, a tool-call entry). Yanking focus away from whatever the user last clicked is surprising and a screen-reader foot-gun; the typical "first activation" case still focuses the terminal because document.activeElement is <body> at that point. Trade-off worth flagging, deliberately not mitigated in this change: while hidden, ChatPage still holds a PTY child + WebSocket + xterm instance for the dashboard's full lifetime. The WS keeps delivering bytes and xterm keeps parsing them into a display:none host (cheap — no paint work, but not free). Reasonable costs to pay for the session preservation; if they become a problem we can pause `term.write` when !isActive or idle-disconnect after N minutes hidden. Lint clean on touched files. tsc -b && vite build pass.
2026-04-27 22:29:17 -04:00
{embeddedChat &&
!chatOverriddenByPlugin &&
(pluginsLoading ? (
isChatRoute ? (
<div
className="flex min-h-0 min-w-0 flex-1 items-center justify-center"
aria-busy="true"
aria-live="polite"
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
2026-04-28 13:59:33 -04:00
<Spinner />
<span>Loading chat</span>
</div>
</div>
) : null
) : (
<div
data-chat-active={isChatRoute ? "true" : "false"}
className={cn(
"min-h-0 min-w-0",
isChatRoute ? "flex flex-1 flex-col" : "hidden",
)}
aria-hidden={!isChatRoute}
>
<ChatPage isActive={isChatRoute} />
</div>
))}
2026-04-22 23:25:17 -04:00
</div>
<PluginSlot name="post-main" />
2026-04-24 10:17:57 -04:00
</div>
2026-04-22 23:25:17 -04:00
</PageHeaderProvider>
</div>
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
</div>
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
feat(dashboard): reskin extension points for themes and plugins (#14776) Themes and plugins can now pull off arbitrary dashboard reskins (cockpit HUD, retro terminal, etc.) without touching core code. Themes gain four new fields: - layoutVariant: standard | cockpit | tiled — shell layout selector - assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} — artwork URLs exposed as --theme-asset-* CSS vars - customCSS: raw CSS injected as a scoped <style> tag on theme apply (32 KiB cap, cleaned up on theme switch) - componentStyles: per-component CSS-var overrides (clipPath, borderImage, background, boxShadow, ...) for card/header/sidebar/ backdrop/tab/progress/badge/footer/page Plugin manifests gain three new fields: - tab.override: replaces a built-in route instead of adding a tab - tab.hidden: register component + slots without adding a nav entry - slots: declares shell slots the plugin populates 10 named shell slots: backdrop, header-left/right/banner, sidebar, pre-main, post-main, footer-left/right, overlay. Plugins register via window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A <PluginSlot> React helper is exported on the plugin SDK. Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML + slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS sidebar with live telemetry, COMPASS crest in header, notched card corners via componentStyles, scanline overlay via customCSS, gold/cyan palette, Orbitron typography. Validation: - 15 new tests in test_web_server.py covering every extended field - tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures) - tsc -b --noEmit: clean - vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions Co-authored-by: Teknium <p@nousresearch.com>
2026-04-23 15:31:01 -07:00
<PluginSlot name="overlay" />
feat: web UI dashboard for managing Hermes Agent (#8756) * feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-04-12 22:26:28 -07:00
</div>
);
}
2026-04-19 10:48:56 -04:00
function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) {
const { path, label, labelKey, icon: Icon } = item;
const navLabel = labelKey
? ((t.app.nav as Record<string, string>)[labelKey] ?? label)
: label;
return (
<li>
<NavLink
to={path}
end={path === "/sessions"}
onClick={closeMobile}
className={({ isActive }) =>
cn(
"group relative flex items-center gap-3",
"px-5 py-2.5",
"font-mondwest 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 }) => (
<>
<Icon className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{navLabel}</span>
<span
aria-hidden
className="absolute inset-y-0.5 left-1.5 right-1.5 bg-midground opacity-0 pointer-events-none transition-opacity duration-200 group-hover:opacity-5"
/>
{isActive && (
<span
aria-hidden
className="absolute left-0 top-0 bottom-0 w-px bg-midground"
style={{ mixBlendMode: "plus-lighter" }}
/>
)}
</>
)}
</NavLink>
</li>
);
}
2026-04-22 23:25:17 -04:00
function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
const { t } = useI18n();
const navigate = useNavigate();
const { activeAction, isBusy, isRunning, pendingAction, runAction } =
useSystemActions();
const items: SystemActionItem[] = [
{
action: "restart",
icon: RotateCw,
label: t.status.restartGateway,
runningLabel: t.status.restartingGateway,
spin: true,
},
{
action: "update",
icon: Download,
label: t.status.updateHermes,
runningLabel: t.status.updatingHermes,
spin: false,
},
];
const handleClick = (action: SystemAction) => {
if (isBusy) return;
void runAction(action);
navigate("/sessions");
onNavigate();
};
return (
<div
className={cn(
"shrink-0 flex flex-col",
"border-t border-current/10",
"py-1",
)}
>
<span
className={cn(
"px-5 pt-0.5 pb-0.5",
"font-mondwest text-[0.6rem] tracking-[0.15em] uppercase opacity-30",
)}
>
{t.app.system}
</span>
<SidebarStatusStrip />
<ul className="flex flex-col">
{items.map(({ action, icon: Icon, label, runningLabel, spin }) => {
const isPending = pendingAction === action;
const isActionRunning =
activeAction === action && isRunning && !isPending;
const busy = isPending || isActionRunning;
const displayLabel = isActionRunning ? runningLabel : label;
const disabled = isBusy && !busy;
return (
<li key={action}>
<ListItem
2026-04-22 23:25:17 -04:00
onClick={() => handleClick(action)}
disabled={disabled}
aria-busy={busy}
active={busy}
2026-04-22 23:25:17 -04:00
className={cn(
"gap-3 px-5 py-1.5 whitespace-nowrap",
2026-04-22 23:25:17 -04:00
"font-mondwest text-[0.75rem] tracking-[0.1em]",
"transition-opacity",
2026-04-22 23:25:17 -04:00
busy
? "text-midground opacity-100"
: "opacity-60 hover:opacity-100",
"disabled:opacity-30",
2026-04-22 23:25:17 -04:00
)}
>
{isPending ? (
2026-04-28 13:59:33 -04:00
<Spinner className="shrink-0 text-[0.875rem]" />
) : isActionRunning && spin ? (
<Spinner className="shrink-0 text-[0.875rem]" />
2026-04-22 23:25:17 -04:00
) : (
<Icon
className={cn(
"h-3.5 w-3.5 shrink-0",
isActionRunning && !spin && "animate-pulse",
)}
/>
)}
<span className="truncate">{displayLabel}</span>
<span
aria-hidden
className="absolute inset-y-0.5 left-1.5 right-1.5 bg-midground opacity-0 pointer-events-none transition-opacity duration-200 group-hover:opacity-5"
/>
{busy && (
<span
aria-hidden
className="absolute left-0 top-0 bottom-0 w-px bg-midground"
style={{ mixBlendMode: "plus-lighter" }}
/>
)}
</ListItem>
2026-04-22 23:25:17 -04:00
</li>
);
})}
</ul>
</div>
);
}
2026-04-19 10:48:56 -04:00
interface NavItem {
2026-04-22 23:25:17 -04:00
icon: ComponentType<{ className?: string }>;
2026-04-19 10:48:56 -04:00
label: string;
labelKey?: string;
path: string;
}
2026-04-22 23:25:17 -04:00
interface SidebarNavLinkProps {
closeMobile: () => void;
item: NavItem;
t: Translations;
}
2026-04-22 23:25:17 -04:00
interface SystemActionItem {
action: SystemAction;
icon: ComponentType<{ className?: string }>;
label: string;
runningLabel: string;
spin: boolean;
}