--- sidebar_position: 16 title: "Dashboard Plugins" description: "Build custom tabs and extensions for the Hermes web dashboard" --- # Dashboard Plugins Dashboard plugins let you add custom tabs to the web dashboard. A plugin can display its own UI, call the Hermes API, and optionally register backend endpoints — all without touching the dashboard source code. ## Quick Start Create a plugin directory with a manifest and a JS file: ```bash mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist ``` **manifest.json:** ```json { "name": "my-plugin", "label": "My Plugin", "icon": "Sparkles", "version": "1.0.0", "tab": { "path": "/my-plugin", "position": "after:skills" }, "entry": "dist/index.js" } ``` **dist/index.js:** ```javascript (function () { var SDK = window.__HERMES_PLUGIN_SDK__; var React = SDK.React; var Card = SDK.components.Card; var CardHeader = SDK.components.CardHeader; var CardTitle = SDK.components.CardTitle; var CardContent = SDK.components.CardContent; function MyPage() { return React.createElement(Card, null, React.createElement(CardHeader, null, React.createElement(CardTitle, null, "My Plugin") ), React.createElement(CardContent, null, React.createElement("p", { className: "text-sm text-muted-foreground" }, "Hello from my custom dashboard tab!" ) ) ); } window.__HERMES_PLUGINS__.register("my-plugin", MyPage); })(); ``` Refresh the dashboard — your tab appears in the navigation bar. ## Plugin Structure Plugins live inside the standard `~/.hermes/plugins/` directory. The dashboard extension is a `dashboard/` subfolder: ``` ~/.hermes/plugins/my-plugin/ plugin.yaml # optional — existing CLI/gateway plugin manifest __init__.py # optional — existing CLI/gateway hooks dashboard/ # dashboard extension manifest.json # required — tab config, icon, entry point dist/ index.js # required — pre-built JS bundle style.css # optional — custom CSS plugin_api.py # optional — backend API routes ``` A single plugin can extend both the CLI/gateway (via `plugin.yaml` + `__init__.py`) and the dashboard (via `dashboard/`) from one directory. ## Manifest Reference The `manifest.json` file describes your plugin to the dashboard: ```json { "name": "my-plugin", "label": "My Plugin", "description": "What this plugin does", "icon": "Sparkles", "version": "1.0.0", "tab": { "path": "/my-plugin", "position": "after:skills" }, "entry": "dist/index.js", "css": "dist/style.css", "api": "plugin_api.py" } ``` | Field | Required | Description | |-------|----------|-------------| | `name` | Yes | Unique plugin identifier (lowercase, hyphens ok) | | `label` | Yes | Display name shown in the nav tab | | `description` | No | Short description | | `icon` | No | Lucide icon name (default: `Puzzle`) | | `version` | No | Semver version string | | `tab.path` | Yes | URL path for the tab (e.g. `/my-plugin`) | | `tab.position` | No | Where to insert the tab: `end` (default), `after:`, `before:` | | `entry` | Yes | Path to the JS bundle relative to `dashboard/` | | `css` | No | Path to a CSS file to inject | | `api` | No | Path to a Python file with FastAPI routes | ### Tab Position The `position` field controls where your tab appears in the navigation: - `"end"` — after all built-in tabs (default) - `"after:skills"` — after the Skills tab - `"before:config"` — before the Config tab - `"after:cron"` — after the Cron tab The value after the colon is the path segment of the target tab (without the leading slash). ### Available Icons Plugins can use any of these Lucide icon names: `Activity`, `BarChart3`, `Clock`, `Code`, `Database`, `Eye`, `FileText`, `Globe`, `Heart`, `KeyRound`, `MessageSquare`, `Package`, `Puzzle`, `Settings`, `Shield`, `Sparkles`, `Star`, `Terminal`, `Wrench`, `Zap` Unrecognized icon names fall back to `Puzzle`. ## Plugin SDK Plugins don't bundle React or UI components — they use the SDK exposed on `window.__HERMES_PLUGIN_SDK__`. This avoids version conflicts and keeps plugin bundles tiny. ### SDK Contents ```javascript var SDK = window.__HERMES_PLUGIN_SDK__; // React SDK.React // React instance SDK.hooks.useState // React hooks SDK.hooks.useEffect SDK.hooks.useCallback SDK.hooks.useMemo SDK.hooks.useRef SDK.hooks.useContext SDK.hooks.createContext // API SDK.api // Hermes API client (getStatus, getSessions, etc.) SDK.fetchJSON // Raw fetch for custom endpoints — handles auth automatically // UI Components (shadcn/ui style) SDK.components.Card SDK.components.CardHeader SDK.components.CardTitle SDK.components.CardContent SDK.components.Badge SDK.components.Button SDK.components.Input SDK.components.Label SDK.components.Select SDK.components.SelectOption SDK.components.Separator SDK.components.Tabs SDK.components.TabsList SDK.components.TabsTrigger // Utilities SDK.utils.cn // Tailwind class merger (clsx + twMerge) SDK.utils.timeAgo // "5m ago" from unix timestamp SDK.utils.isoTimeAgo // "5m ago" from ISO string // Hooks SDK.useI18n // i18n translations SDK.useTheme // Current theme info ``` ### Using SDK.fetchJSON For calling your plugin's backend API endpoints: ```javascript SDK.fetchJSON("/api/plugins/my-plugin/data") .then(function (result) { console.log(result); }) .catch(function (err) { console.error("API call failed:", err); }); ``` `fetchJSON` automatically injects the session auth token, handles errors, and parses JSON. ### Using Existing API Methods The `SDK.api` object has methods for all built-in Hermes endpoints: ```javascript // Fetch agent status SDK.api.getStatus().then(function (status) { console.log("Version:", status.version); }); // List sessions SDK.api.getSessions(10).then(function (resp) { console.log("Sessions:", resp.sessions.length); }); ``` ## Backend API Routes Plugins can register FastAPI routes by setting the `api` field in the manifest. Create a Python file that exports a `router`: ```python # plugin_api.py from fastapi import APIRouter router = APIRouter() @router.get("/data") async def get_data(): return {"items": ["one", "two", "three"]} @router.post("/action") async def do_action(body: dict): return {"ok": True, "received": body} ``` Routes are mounted at `/api/plugins//`, so the above becomes: - `GET /api/plugins/my-plugin/data` - `POST /api/plugins/my-plugin/action` Plugin API routes bypass session token authentication since the dashboard server only binds to localhost. ### Accessing Hermes Internals Backend routes can import from the hermes-agent codebase: ```python from fastapi import APIRouter from hermes_state import SessionDB from hermes_cli.config import load_config router = APIRouter() @router.get("/session-count") async def session_count(): db = SessionDB() try: count = len(db.list_sessions(limit=9999)) return {"count": count} finally: db.close() ``` ## Custom CSS If your plugin needs custom styles, add a CSS file and reference it in the manifest: ```json { "css": "dist/style.css" } ``` The CSS file is injected as a `` tag when the plugin loads. Use specific class names to avoid conflicts with the dashboard's existing styles. ```css /* dist/style.css */ .my-plugin-chart { border: 1px solid var(--color-border); background: var(--color-card); padding: 1rem; } ``` You can use the dashboard's CSS custom properties (e.g. `--color-border`, `--color-foreground`) to match the active theme. ## Plugin Loading Flow 1. Dashboard loads — `main.tsx` exposes the SDK on `window.__HERMES_PLUGIN_SDK__` 2. `App.tsx` calls `usePlugins()` which fetches `GET /api/dashboard/plugins` 3. For each plugin: CSS `` injected (if declared), JS `