mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
docs: consolidate dashboard themes and plugins into Extending the Dashboard (#15530)
The web-dashboard.md and dashboard-plugins.md pages had overlapping, partial coverage of the theme and plugin systems. Themes were split across two pages; the plugin docs had a minimal manifest reference but no step-by-step guide, no slot catalog, and no theme+plugin demo. New: user-guide/features/extending-the-dashboard.md — single navigable reference for all three extension layers (themes, UI plugins, backend plugins). Includes: - Theme quick-start + full schema (palette, typography, layout, layout variants, assets, componentStyles, colorOverrides, customCSS) - Plugin quick-start + full schema (manifest, SDK, slots, tab.override, tab.hidden, backend routes, custom CSS) - 10-slot shell catalog with locations - Plugin discovery + load lifecycle - Combined theme+plugin walkthrough (Strike Freedom cockpit demo) - API reference + troubleshooting web-dashboard.md: trimmed to core tool docs (pages, REST API, CORS, development). Theme/plugin content now points to the new page with a built-in themes summary table. dashboard-plugins.md: deleted (merged into extending-the-dashboard.md). sidebars.ts: swap 'dashboard-plugins' → 'extending-the-dashboard' under the Management group. No user-facing behavior change; docs-only.
This commit is contained in:
parent
023b1bff11
commit
e5647d7863
4 changed files with 837 additions and 602 deletions
|
|
@ -1,336 +0,0 @@
|
|||
---
|
||||
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:<tab>`, `before:<tab>` |
|
||||
| `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/<name>/`, 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 `<link>` 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 `<link>` injected (if declared), JS `<script>` loaded
|
||||
4. Plugin JS calls `window.__HERMES_PLUGINS__.register(name, Component)`
|
||||
5. Dashboard adds the tab to navigation and mounts the component as a route
|
||||
|
||||
Plugins have up to 2 seconds to register after their script loads. If a plugin fails to load, the dashboard continues without it.
|
||||
|
||||
## Plugin Discovery
|
||||
|
||||
The dashboard scans these directories for `dashboard/manifest.json`:
|
||||
|
||||
1. **User plugins:** `~/.hermes/plugins/<name>/dashboard/manifest.json`
|
||||
2. **Bundled plugins:** `<repo>/plugins/<name>/dashboard/manifest.json`
|
||||
3. **Project plugins:** `./.hermes/plugins/<name>/dashboard/manifest.json` (only when `HERMES_ENABLE_PROJECT_PLUGINS` is set)
|
||||
|
||||
User plugins take precedence — if the same plugin name exists in multiple sources, the user version wins.
|
||||
|
||||
To force re-scanning after adding a new plugin without restarting the server:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
|
||||
```
|
||||
|
||||
## Plugin API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/dashboard/plugins` | GET | List discovered plugins |
|
||||
| `/api/dashboard/plugins/rescan` | GET | Force re-scan for new plugins |
|
||||
| `/dashboard-plugins/<name>/<path>` | GET | Serve plugin static assets |
|
||||
| `/api/plugins/<name>/*` | * | Plugin-registered API routes |
|
||||
|
||||
## Example Plugin
|
||||
|
||||
The repository includes an example plugin at `plugins/example-dashboard/` that demonstrates:
|
||||
|
||||
- Using SDK components (Card, Badge, Button)
|
||||
- Calling a backend API route
|
||||
- Registering via `window.__HERMES_PLUGINS__.register()`
|
||||
|
||||
To try it, run `hermes dashboard` — the "Example" tab appears after Skills.
|
||||
|
||||
## Tips
|
||||
|
||||
- **No build step required** — write plain JavaScript IIFEs. If you prefer JSX, use any bundler (esbuild, Vite, webpack) targeting IIFE output with React as an external.
|
||||
- **Keep bundles small** — React and all UI components are provided by the SDK. Your bundle should only contain your plugin logic.
|
||||
- **Use theme variables** — reference `var(--color-*)` in CSS to automatically match whatever theme the user has selected.
|
||||
- **Test locally** — run `hermes dashboard --no-open` and use browser dev tools to verify your plugin loads and registers correctly.
|
||||
818
website/docs/user-guide/features/extending-the-dashboard.md
Normal file
818
website/docs/user-guide/features/extending-the-dashboard.md
Normal file
|
|
@ -0,0 +1,818 @@
|
|||
---
|
||||
sidebar_position: 17
|
||||
title: "Extending the Dashboard"
|
||||
description: "Build themes and plugins for the Hermes web dashboard — palettes, typography, layouts, custom tabs, shell slots, and backend API routes"
|
||||
---
|
||||
|
||||
# Extending the Dashboard
|
||||
|
||||
The Hermes web dashboard (`hermes dashboard`) is built to be reskinned and extended without forking the codebase. Three layers are exposed:
|
||||
|
||||
1. **Themes** — YAML files that repaint the dashboard's palette, typography, layout, and per-component chrome. Drop a file in `~/.hermes/dashboard-themes/`; it appears in the theme switcher.
|
||||
2. **UI plugins** — a directory with `manifest.json` + a JavaScript bundle that registers a tab, replaces a built-in page, or injects components into named shell slots.
|
||||
3. **Backend plugins** — a Python file inside that plugin directory that exposes a FastAPI `router`; routes are mounted under `/api/plugins/<name>/` and called from the plugin's UI.
|
||||
|
||||
All three are **drop-in at runtime**: no repo clone, no `npm run build`, no patching the dashboard source. This page is the canonical reference for all three.
|
||||
|
||||
If you just want to use the dashboard, see [Web Dashboard](./web-dashboard). If you want to reskin the terminal CLI (not the web dashboard), see [Skins & Themes](./skins) — the CLI skin system is unrelated to dashboard themes.
|
||||
|
||||
:::note How the pieces compose
|
||||
Themes and plugins are independent but synergistic. A theme can stand alone (just a YAML file). A plugin can stand alone (just a tab). Together they let you build a complete visual reskin with custom HUDs — the bundled `strike-freedom-cockpit` demo does exactly that. See [Combined theme + plugin demo](#combined-theme--plugin-demo).
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Themes](#themes)
|
||||
- [Quick start — your first theme](#quick-start--your-first-theme)
|
||||
- [Palette, typography, layout](#palette-typography-layout)
|
||||
- [Layout variants](#layout-variants)
|
||||
- [Theme assets (images as CSS vars)](#theme-assets-images-as-css-vars)
|
||||
- [Component chrome overrides](#component-chrome-overrides)
|
||||
- [Color overrides](#color-overrides)
|
||||
- [Raw `customCSS`](#raw-customcss)
|
||||
- [Built-in themes](#built-in-themes)
|
||||
- [Full theme YAML reference](#full-theme-yaml-reference)
|
||||
- [Plugins](#plugins)
|
||||
- [Quick start — your first plugin](#quick-start--your-first-plugin)
|
||||
- [Directory layout](#directory-layout)
|
||||
- [Manifest reference](#manifest-reference)
|
||||
- [The Plugin SDK](#the-plugin-sdk)
|
||||
- [Shell slots](#shell-slots)
|
||||
- [Replacing built-in pages (`tab.override`)](#replacing-built-in-pages-taboverride)
|
||||
- [Slot-only plugins (`tab.hidden`)](#slot-only-plugins-tabhidden)
|
||||
- [Backend API routes](#backend-api-routes)
|
||||
- [Custom CSS per plugin](#custom-css-per-plugin)
|
||||
- [Plugin discovery & reload](#plugin-discovery--reload)
|
||||
- [Combined theme + plugin demo](#combined-theme--plugin-demo)
|
||||
- [API reference](#api-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Themes
|
||||
|
||||
Themes are YAML files stored in `~/.hermes/dashboard-themes/`. The file name doesn't matter (the theme's `name:` field is what the system uses), but convention is `<name>.yaml`. Every field is optional — missing keys fall back to the built-in `default` theme, so a theme can be as small as one color.
|
||||
|
||||
### Quick start — your first theme
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.hermes/dashboard-themes
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/dashboard-themes/neon.yaml
|
||||
name: neon
|
||||
label: Neon
|
||||
description: Pure magenta on black
|
||||
|
||||
palette:
|
||||
background: "#000000"
|
||||
midground: "#ff00ff"
|
||||
```
|
||||
|
||||
Refresh the dashboard. Click the palette icon in the header and pick **Neon**. The background goes black, text and accents go magenta, and every derived color (card, border, muted, ring, etc.) is recomputed from that 2-color triplet via `color-mix()` in CSS.
|
||||
|
||||
That's the whole onboarding: one file, two colors. Everything below is optional refinement.
|
||||
|
||||
### Palette, typography, layout
|
||||
|
||||
These three blocks are the heart of a theme. Each is independent — override one, leave the others.
|
||||
|
||||
#### Palette (3-layer)
|
||||
|
||||
The palette is a triplet of color layers plus a warm-glow vignette color and a noise-grain multiplier. The dashboard's design-system cascade derives every shadcn-compatible token (card, popover, muted, border, primary, destructive, ring, etc.) from this triplet via CSS `color-mix()`. Overriding three colors cascades into the whole UI.
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `palette.background` | Deepest canvas color — typically near-black. Drives the page background and card fill. |
|
||||
| `palette.midground` | Primary text and accent. Most UI chrome reads this (foreground text, button outlines, focus rings). |
|
||||
| `palette.foreground` | Top-layer highlight. The default theme sets this to white at alpha 0 (invisible); themes that want a bright accent on top can raise its alpha. |
|
||||
| `palette.warmGlow` | `rgba(...)` string used as the vignette color by `<Backdrop />`. |
|
||||
| `palette.noiseOpacity` | 0–1.2 multiplier on the grain overlay. Lower = softer, higher = grittier. |
|
||||
|
||||
Each layer accepts either `{hex: "#RRGGBB", alpha: 0.0–1.0}` or a bare hex string (alpha defaults to 1.0).
|
||||
|
||||
```yaml
|
||||
palette:
|
||||
background:
|
||||
hex: "#05091a"
|
||||
alpha: 1.0
|
||||
midground: "#d8f0ff" # bare hex, alpha = 1.0
|
||||
foreground:
|
||||
hex: "#ffffff"
|
||||
alpha: 0 # invisible top layer
|
||||
warmGlow: "rgba(255, 199, 55, 0.24)"
|
||||
noiseOpacity: 0.7
|
||||
```
|
||||
|
||||
#### Typography
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `fontSans` | string | CSS font-family stack for body copy (applied to `html`, `body`). |
|
||||
| `fontMono` | string | CSS font-family stack for code blocks, `<code>`, `.font-mono` utilities. |
|
||||
| `fontDisplay` | string | Optional heading/display stack. Falls back to `fontSans`. |
|
||||
| `fontUrl` | string | Optional external stylesheet URL. Injected as `<link rel="stylesheet">` in `<head>` on theme switch. Same URL is never injected twice. Works with Google Fonts, Bunny Fonts, self-hosted `@font-face` sheets — anything linkable. |
|
||||
| `baseSize` | string | Root font size — controls the rem scale. E.g. `"14px"`, `"16px"`. |
|
||||
| `lineHeight` | string | Default line-height. E.g. `"1.5"`, `"1.65"`. |
|
||||
| `letterSpacing` | string | Default letter-spacing. E.g. `"0"`, `"0.01em"`, `"-0.01em"`. |
|
||||
|
||||
```yaml
|
||||
typography:
|
||||
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
|
||||
fontMono: '"Share Tech Mono", ui-monospace, monospace'
|
||||
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
|
||||
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
|
||||
baseSize: "14px"
|
||||
lineHeight: "1.5"
|
||||
letterSpacing: "0.04em"
|
||||
```
|
||||
|
||||
#### Layout
|
||||
|
||||
| Key | Values | Description |
|
||||
|-----|--------|-------------|
|
||||
| `radius` | any CSS length (`"0"`, `"0.25rem"`, `"0.5rem"`, `"1rem"`, ...) | Corner-radius token. Maps to `--radius` and cascades into `--radius-sm/md/lg/xl` — every rounded element shifts together. |
|
||||
| `density` | `compact` \| `comfortable` \| `spacious` | Spacing multiplier applied as the `--spacing-mul` CSS var. `compact = 0.85×`, `comfortable = 1.0×` (default), `spacious = 1.2×`. Scales Tailwind's base spacing, so padding, gap, and space-between utilities all shift proportionally. |
|
||||
|
||||
```yaml
|
||||
layout:
|
||||
radius: "0"
|
||||
density: compact
|
||||
```
|
||||
|
||||
### Layout variants
|
||||
|
||||
`layoutVariant` picks the overall shell layout. Defaults to `"standard"` when absent.
|
||||
|
||||
| Variant | Behaviour |
|
||||
|---------|-----------|
|
||||
| `standard` | Single column, 1600px max-width (default). |
|
||||
| `cockpit` | Left sidebar rail (260px) + main content. Populated by plugins via the `sidebar` slot — see [Shell slots](#shell-slots). Without a plugin the rail shows a placeholder. |
|
||||
| `tiled` | Drops the max-width clamp so pages can use the full viewport width. |
|
||||
|
||||
```yaml
|
||||
layoutVariant: cockpit
|
||||
```
|
||||
|
||||
The current variant is exposed as `document.documentElement.dataset.layoutVariant`, so raw CSS in `customCSS` can target it via `:root[data-layout-variant="cockpit"] ...`.
|
||||
|
||||
### Theme assets (images as CSS vars)
|
||||
|
||||
Ship artwork URLs with a theme. Each named slot becomes a CSS var (`--theme-asset-<name>`) that the built-in shell and any plugin can read. The `bg` slot is automatically wired into the backdrop; other slots are plugin-facing.
|
||||
|
||||
```yaml
|
||||
assets:
|
||||
bg: "https://example.com/hero-bg.jpg" # auto-wired into <Backdrop />
|
||||
hero: "/my-images/strike-freedom.png" # for plugin sidebars
|
||||
crest: "/my-images/crest.svg" # for header-left plugins
|
||||
logo: "/my-images/logo.png"
|
||||
sidebar: "/my-images/rail.png"
|
||||
header: "/my-images/header-art.png"
|
||||
custom:
|
||||
scanLines: "/my-images/scanlines.png" # → --theme-asset-custom-scanLines
|
||||
```
|
||||
|
||||
Values accept:
|
||||
|
||||
- Bare URLs — wrapped in `url(...)` automatically.
|
||||
- Pre-wrapped `url(...)`, `linear-gradient(...)`, `radial-gradient(...)` expressions — used as-is.
|
||||
- `"none"` — explicit opt-out.
|
||||
|
||||
Every asset is also emitted as `--theme-asset-<name>-raw` (the unwrapped URL), in case a plugin needs to pass it to `<img src>` instead of `background-image`.
|
||||
|
||||
Plugins read these with plain CSS or JS:
|
||||
|
||||
```javascript
|
||||
// In a plugin slot
|
||||
const hero = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--theme-asset-hero").trim();
|
||||
```
|
||||
|
||||
### Component chrome overrides
|
||||
|
||||
`componentStyles` restyles individual shell components without writing CSS selectors. Each bucket's entries become CSS vars (`--component-<bucket>-<kebab-property>`) that the shell's shared components read. So `card:` overrides apply to every `<Card>`, `header:` to the app bar, etc.
|
||||
|
||||
```yaml
|
||||
componentStyles:
|
||||
card:
|
||||
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
|
||||
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
|
||||
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
|
||||
header:
|
||||
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
|
||||
tab:
|
||||
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
|
||||
sidebar: {}
|
||||
backdrop: {}
|
||||
footer: {}
|
||||
progress: {}
|
||||
badge: {}
|
||||
page: {}
|
||||
```
|
||||
|
||||
Supported buckets: `card`, `header`, `footer`, `sidebar`, `tab`, `progress`, `badge`, `backdrop`, `page`.
|
||||
|
||||
Property names use camelCase (`clipPath`) and are emitted as kebab (`clip-path`). Values are plain CSS strings — anything CSS accepts (`clip-path`, `border-image`, `background`, `box-shadow`, `animation`, ...).
|
||||
|
||||
### Color overrides
|
||||
|
||||
Most themes won't need this — the 3-layer palette derives every shadcn token. Use `colorOverrides` when you want a specific accent the derivation won't produce (a softer destructive red for a pastel theme, a specific success green for a brand).
|
||||
|
||||
```yaml
|
||||
colorOverrides:
|
||||
primary: "#ffce3a"
|
||||
primaryForeground: "#05091a"
|
||||
accent: "#3fd3ff"
|
||||
ring: "#3fd3ff"
|
||||
destructive: "#ff3a5e"
|
||||
border: "rgba(64, 200, 255, 0.28)"
|
||||
```
|
||||
|
||||
Supported keys: `card`, `cardForeground`, `popover`, `popoverForeground`, `primary`, `primaryForeground`, `secondary`, `secondaryForeground`, `muted`, `mutedForeground`, `accent`, `accentForeground`, `destructive`, `destructiveForeground`, `success`, `warning`, `border`, `input`, `ring`.
|
||||
|
||||
Each key maps 1:1 to the `--color-<kebab>` CSS var (e.g. `primaryForeground` → `--color-primary-foreground`). Any key set here wins over the palette cascade for the active theme only — switching to another theme clears the overrides.
|
||||
|
||||
### Raw `customCSS`
|
||||
|
||||
For selector-level chrome that `componentStyles` can't express — pseudo-elements, animations, media queries, theme-scoped overrides — drop raw CSS into `customCSS`:
|
||||
|
||||
```yaml
|
||||
customCSS: |
|
||||
/* Scanline overlay — only visible when cockpit variant is active. */
|
||||
:root[data-layout-variant="cockpit"] body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
transparent 0px, transparent 2px,
|
||||
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
```
|
||||
|
||||
The CSS is injected as a single scoped `<style data-hermes-theme-css>` tag on theme apply and cleaned up on theme switch. **Capped at 32 KiB per theme.**
|
||||
|
||||
### Built-in themes
|
||||
|
||||
Each built-in ships its own palette, typography, and layout — switching produces visible changes beyond color alone.
|
||||
|
||||
| Theme | Palette | Typography | Layout |
|
||||
|-------|---------|------------|--------|
|
||||
| **Hermes Teal** (`default`) | Dark teal + cream | System stack, 15px | 0.5rem radius, comfortable |
|
||||
| **Midnight** (`midnight`) | Deep blue-violet | Inter + JetBrains Mono, 14px | 0.75rem radius, comfortable |
|
||||
| **Ember** (`ember`) | Warm crimson + bronze | Spectral (serif) + IBM Plex Mono, 15px | 0.25rem radius, comfortable |
|
||||
| **Mono** (`mono`) | Grayscale | IBM Plex Sans + IBM Plex Mono, 13px | 0 radius, compact |
|
||||
| **Cyberpunk** (`cyberpunk`) | Neon green on black | Share Tech Mono everywhere, 14px | 0 radius, compact |
|
||||
| **Rosé** (`rose`) | Pink + ivory | Fraunces (serif) + DM Mono, 16px | 1rem radius, spacious |
|
||||
|
||||
Themes that reference Google Fonts (all except Hermes Teal) load the stylesheet on demand — the first time you switch to them a `<link>` tag is injected into `<head>`.
|
||||
|
||||
### Full theme YAML reference
|
||||
|
||||
Every knob in one file — copy and trim what you don't need:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/dashboard-themes/ocean.yaml
|
||||
name: ocean
|
||||
label: Ocean Deep
|
||||
description: Deep sea blues with coral accents
|
||||
|
||||
# 3-layer palette (accepts {hex, alpha} or bare hex)
|
||||
palette:
|
||||
background:
|
||||
hex: "#0a1628"
|
||||
alpha: 1.0
|
||||
midground:
|
||||
hex: "#a8d0ff"
|
||||
alpha: 1.0
|
||||
foreground:
|
||||
hex: "#ffffff"
|
||||
alpha: 0.0
|
||||
warmGlow: "rgba(255, 107, 107, 0.35)"
|
||||
noiseOpacity: 0.7
|
||||
|
||||
typography:
|
||||
fontSans: "Poppins, system-ui, sans-serif"
|
||||
fontMono: "Fira Code, ui-monospace, monospace"
|
||||
fontDisplay: "Poppins, system-ui, sans-serif" # optional
|
||||
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
|
||||
baseSize: "15px"
|
||||
lineHeight: "1.6"
|
||||
letterSpacing: "-0.003em"
|
||||
|
||||
layout:
|
||||
radius: "0.75rem"
|
||||
density: comfortable
|
||||
|
||||
layoutVariant: standard # standard | cockpit | tiled
|
||||
|
||||
assets:
|
||||
bg: "https://example.com/ocean-bg.jpg"
|
||||
hero: "/my-images/kraken.png"
|
||||
crest: "/my-images/anchor.svg"
|
||||
logo: "/my-images/logo.png"
|
||||
custom:
|
||||
pattern: "/my-images/waves.svg"
|
||||
|
||||
componentStyles:
|
||||
card:
|
||||
boxShadow: "inset 0 0 0 1px rgba(168, 208, 255, 0.18)"
|
||||
header:
|
||||
background: "linear-gradient(180deg, rgba(10, 22, 40, 0.95), rgba(5, 9, 26, 0.9))"
|
||||
|
||||
colorOverrides:
|
||||
destructive: "#ff6b6b"
|
||||
ring: "#ff6b6b"
|
||||
|
||||
customCSS: |
|
||||
/* Any additional selector-level tweaks */
|
||||
```
|
||||
|
||||
Refresh the dashboard after creating the file. Switch themes live from the header bar — click the palette icon. Selection persists to `config.yaml` under `dashboard.theme` and is restored on reload.
|
||||
|
||||
---
|
||||
|
||||
## Plugins
|
||||
|
||||
A dashboard plugin is a directory with a `manifest.json`, a pre-built JS bundle, and optionally a CSS file and a Python file with FastAPI routes. Plugins live next to other Hermes plugins in `~/.hermes/plugins/<name>/` — the dashboard extension is a `dashboard/` subfolder inside that plugin directory, so one plugin can extend both the CLI/gateway and the dashboard from a single install.
|
||||
|
||||
Plugins don't bundle React or UI components. They use the **Plugin SDK** exposed on `window.__HERMES_PLUGIN_SDK__`. This keeps plugin bundles tiny (typically a few KB) and avoids version conflicts.
|
||||
|
||||
### Quick start — your first plugin
|
||||
|
||||
Create the directory structure:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist
|
||||
```
|
||||
|
||||
Write the manifest:
|
||||
|
||||
```json
|
||||
// ~/.hermes/plugins/my-plugin/dashboard/manifest.json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"label": "My Plugin",
|
||||
"icon": "Sparkles",
|
||||
"version": "1.0.0",
|
||||
"tab": {
|
||||
"path": "/my-plugin",
|
||||
"position": "after:skills"
|
||||
},
|
||||
"entry": "dist/index.js"
|
||||
}
|
||||
```
|
||||
|
||||
Write the JS bundle (a plain IIFE — no build step needed):
|
||||
|
||||
```javascript
|
||||
// ~/.hermes/plugins/my-plugin/dashboard/dist/index.js
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const SDK = window.__HERMES_PLUGIN_SDK__;
|
||||
const { React } = SDK;
|
||||
const { Card, CardHeader, CardTitle, CardContent } = SDK.components;
|
||||
|
||||
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 nav bar, after **Skills**.
|
||||
|
||||
:::tip Skip React.createElement
|
||||
If you prefer JSX, use any bundler (esbuild, Vite, rollup) with React as an external and IIFE output. The only hard requirement is that the final file is a single JS file loadable via `<script>`. React is never bundled; it comes from `SDK.React`.
|
||||
:::
|
||||
|
||||
### Directory layout
|
||||
|
||||
```
|
||||
~/.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 (IIFE)
|
||||
│ └── style.css # optional — custom CSS
|
||||
└── plugin_api.py # optional — backend API routes (FastAPI)
|
||||
```
|
||||
|
||||
A single plugin directory can carry three orthogonal extensions:
|
||||
|
||||
- `plugin.yaml` + `__init__.py` — CLI/gateway plugin ([see plugins page](./plugins)).
|
||||
- `dashboard/manifest.json` + `dashboard/dist/index.js` — dashboard UI plugin.
|
||||
- `dashboard/plugin_api.py` — dashboard backend routes.
|
||||
|
||||
None of them are required; include only the layers you need.
|
||||
|
||||
### Manifest reference
|
||||
|
||||
```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",
|
||||
"override": "/",
|
||||
"hidden": false
|
||||
},
|
||||
"slots": ["sidebar", "header-left"],
|
||||
"entry": "dist/index.js",
|
||||
"css": "dist/style.css",
|
||||
"api": "plugin_api.py"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Unique plugin identifier. Lowercase, hyphens ok. Used in URLs and registration. |
|
||||
| `label` | Yes | Display name shown in the nav tab. |
|
||||
| `description` | No | Short description (shown in dashboard admin surfaces). |
|
||||
| `icon` | No | Lucide icon name. Defaults to `Puzzle`. Unknown names fall back to `Puzzle`. |
|
||||
| `version` | No | Semver string. Defaults to `0.0.0`. |
|
||||
| `tab.path` | Yes | URL path for the tab (e.g. `/my-plugin`). |
|
||||
| `tab.position` | No | Where to insert the tab. `"end"` (default), `"after:<path>"`, or `"before:<path>"` — value after the colon is the **path segment** of the target tab (no leading slash). Examples: `"after:skills"`, `"before:config"`. |
|
||||
| `tab.override` | No | Set to a built-in route path (`"/"`, `"/sessions"`, `"/config"`, ...) to **replace** that page instead of adding a new tab. See [Replacing built-in pages](#replacing-built-in-pages-taboverride). |
|
||||
| `tab.hidden` | No | When true, register the component and any slots without adding a tab to the nav. Used by slot-only plugins. See [Slot-only plugins](#slot-only-plugins-tabhidden). |
|
||||
| `slots` | No | Named shell slots this plugin populates. **Documentation aid only** — actual registration happens from the JS bundle via `registerSlot()`. Listing slots here makes discovery surfaces more informative. |
|
||||
| `entry` | Yes | Path to the JS bundle relative to `dashboard/`. Defaults to `dist/index.js`. |
|
||||
| `css` | No | Path to a CSS file to inject as a `<link>` tag. |
|
||||
| `api` | No | Path to a Python file with FastAPI routes. Mounted at `/api/plugins/<name>/`. |
|
||||
|
||||
#### Available icons
|
||||
|
||||
Plugins use Lucide icon names. The dashboard maps these by name — unknown names silently fall back to `Puzzle`.
|
||||
|
||||
Currently mapped: `Activity`, `BarChart3`, `Clock`, `Code`, `Database`, `Eye`, `FileText`, `Globe`, `Heart`, `KeyRound`, `MessageSquare`, `Package`, `Puzzle`, `Settings`, `Shield`, `Sparkles`, `Star`, `Terminal`, `Wrench`, `Zap`.
|
||||
|
||||
Need a different icon? Open a PR to `web/src/App.tsx`'s `ICON_MAP` — pure additive change.
|
||||
|
||||
### The Plugin SDK
|
||||
|
||||
Everything a plugin needs is on `window.__HERMES_PLUGIN_SDK__`. Plugins should never import React directly.
|
||||
|
||||
```javascript
|
||||
const SDK = window.__HERMES_PLUGIN_SDK__;
|
||||
|
||||
// React + hooks
|
||||
SDK.React // the React instance
|
||||
SDK.hooks.useState
|
||||
SDK.hooks.useEffect
|
||||
SDK.hooks.useCallback
|
||||
SDK.hooks.useMemo
|
||||
SDK.hooks.useRef
|
||||
SDK.hooks.useContext
|
||||
SDK.hooks.createContext
|
||||
|
||||
// UI components (shadcn/ui primitives)
|
||||
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
|
||||
SDK.components.PluginSlot // render a named slot (useful for nested plugin UIs)
|
||||
|
||||
// Hermes API client + raw fetcher
|
||||
SDK.api // typed client — getStatus, getSessions, getConfig, ...
|
||||
SDK.fetchJSON // raw fetch for custom endpoints (plugin-registered routes)
|
||||
|
||||
// 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 hook for multi-language plugins
|
||||
```
|
||||
|
||||
#### Calling your plugin's backend
|
||||
|
||||
```javascript
|
||||
SDK.fetchJSON("/api/plugins/my-plugin/data")
|
||||
.then((data) => console.log(data))
|
||||
.catch((err) => console.error("API call failed:", err));
|
||||
```
|
||||
|
||||
`fetchJSON` injects the session auth token, surfaces errors as thrown exceptions, and parses JSON automatically.
|
||||
|
||||
#### Calling built-in Hermes endpoints
|
||||
|
||||
```javascript
|
||||
// Agent status
|
||||
SDK.api.getStatus().then((s) => console.log("Version:", s.version));
|
||||
|
||||
// Recent sessions
|
||||
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));
|
||||
```
|
||||
|
||||
See [Web Dashboard → REST API](./web-dashboard#rest-api) for the full list.
|
||||
|
||||
### Shell slots
|
||||
|
||||
Slots let a plugin inject components into named locations of the app shell — the cockpit sidebar, the header, the footer, an overlay layer — without claiming a whole tab. Multiple plugins can populate the same slot; they render stacked in registration order.
|
||||
|
||||
Register from inside the plugin bundle:
|
||||
|
||||
```javascript
|
||||
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sidebar", MySidebar);
|
||||
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "header-left", MyCrest);
|
||||
```
|
||||
|
||||
#### Slot catalogue
|
||||
|
||||
| Slot | Location |
|
||||
|------|----------|
|
||||
| `backdrop` | Inside the `<Backdrop />` layer stack, above the noise layer. |
|
||||
| `header-left` | Before the Hermes brand in the top bar. |
|
||||
| `header-right` | Before the theme/language switchers in the top bar. |
|
||||
| `header-banner` | Full-width strip below the nav. |
|
||||
| `sidebar` | Cockpit sidebar rail — **only rendered when `layoutVariant === "cockpit"`**. |
|
||||
| `pre-main` | Above the route outlet (inside `<main>`). |
|
||||
| `post-main` | Below the route outlet (inside `<main>`). |
|
||||
| `footer-left` | Footer cell content (replaces default). |
|
||||
| `footer-right` | Footer cell content (replaces default). |
|
||||
| `overlay` | Fixed-position layer above everything else. Useful for chrome (scanlines, vignettes) `customCSS` can't achieve alone. |
|
||||
|
||||
The shell only renders `<PluginSlot name="..." />` for the slots above. Additional names are accepted by the registry for nested plugin UIs — a plugin can expose its own slots via `SDK.components.PluginSlot`.
|
||||
|
||||
#### Re-registration and HMR
|
||||
|
||||
If the same `(plugin, slot)` pair is registered twice, the later call replaces the earlier one — this matches how React HMR expects plugin re-mounts to behave.
|
||||
|
||||
### Replacing built-in pages (`tab.override`)
|
||||
|
||||
Setting `tab.override` to a built-in route path makes the plugin's component replace that page instead of adding a new tab. Useful when a theme wants a custom home page (`/`) but wants to keep the rest of the dashboard intact.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-home",
|
||||
"label": "Home",
|
||||
"tab": {
|
||||
"path": "/my-home",
|
||||
"override": "/",
|
||||
"position": "end"
|
||||
},
|
||||
"entry": "dist/index.js"
|
||||
}
|
||||
```
|
||||
|
||||
With `override` set:
|
||||
|
||||
- The original page component at `/` is removed from the router.
|
||||
- Your plugin renders at `/` instead.
|
||||
- No nav tab is added for `tab.path` (the override is the point).
|
||||
|
||||
Only one plugin can override a given path. If two plugins claim the same override, the first wins and the second is ignored with a dev-mode warning.
|
||||
|
||||
### Slot-only plugins (`tab.hidden`)
|
||||
|
||||
When `tab.hidden: true`, the plugin registers its component (for direct URL visits) and any slots, but never adds a tab to the navigation. Used by plugins that only exist to inject into slots — a header crest, a sidebar HUD, an overlay.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "header-crest",
|
||||
"label": "Header Crest",
|
||||
"tab": {
|
||||
"path": "/header-crest",
|
||||
"position": "end",
|
||||
"hidden": true
|
||||
},
|
||||
"slots": ["header-left"],
|
||||
"entry": "dist/index.js"
|
||||
}
|
||||
```
|
||||
|
||||
The bundle still calls `register()` with a placeholder component (good practice in case someone hits the URL directly) and then `registerSlot()` to do the real work.
|
||||
|
||||
### Backend API routes
|
||||
|
||||
Plugins can register FastAPI routes by setting `api` in the manifest. Create the file and export a `router`:
|
||||
|
||||
```python
|
||||
# ~/.hermes/plugins/my-plugin/dashboard/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 under `/api/plugins/<name>/`, 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 binds to localhost by default. **Don't expose the dashboard on a public interface with `--host 0.0.0.0` if you run untrusted plugins** — their routes become reachable too.
|
||||
|
||||
#### Accessing Hermes internals
|
||||
|
||||
Backend routes run inside the dashboard process, so they can import from the hermes-agent codebase directly:
|
||||
|
||||
```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()
|
||||
|
||||
@router.get("/config-snapshot")
|
||||
async def config_snapshot():
|
||||
cfg = load_config()
|
||||
return {"model": cfg.get("model", {})}
|
||||
```
|
||||
|
||||
### Custom CSS per plugin
|
||||
|
||||
If your plugin needs styles beyond Tailwind classes and inline `style=`, add a CSS file and reference it in the manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"css": "dist/style.css"
|
||||
}
|
||||
```
|
||||
|
||||
The file is injected as a `<link>` tag on plugin load. Use specific class names to avoid conflicts with the dashboard's styles, and reference the dashboard's CSS vars to stay theme-aware:
|
||||
|
||||
```css
|
||||
/* dist/style.css */
|
||||
.my-plugin-chart {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-card);
|
||||
color: var(--color-card-foreground);
|
||||
padding: 1rem;
|
||||
}
|
||||
.my-plugin-chart:hover {
|
||||
border-color: var(--color-ring);
|
||||
}
|
||||
```
|
||||
|
||||
The dashboard exposes every shadcn token as `--color-*` plus theme extras (`--theme-asset-*`, `--component-<bucket>-*`, `--radius`, `--spacing-mul`). Reference those and your plugin automatically reskins with the active theme.
|
||||
|
||||
### Plugin discovery & reload
|
||||
|
||||
The dashboard scans three directories for `dashboard/manifest.json`:
|
||||
|
||||
| Priority | Directory | Source label |
|
||||
|----------|-----------|--------------|
|
||||
| 1 (wins on conflict) | `~/.hermes/plugins/<name>/dashboard/` | `user` |
|
||||
| 2 | `<repo>/plugins/memory/<name>/dashboard/` | `bundled` |
|
||||
| 2 | `<repo>/plugins/<name>/dashboard/` | `bundled` |
|
||||
| 3 | `./.hermes/plugins/<name>/dashboard/` | `project` — only when `HERMES_ENABLE_PROJECT_PLUGINS` is set |
|
||||
|
||||
Discovery results are cached per dashboard process. After adding a new plugin, either:
|
||||
|
||||
```bash
|
||||
# Force a rescan without restart
|
||||
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
|
||||
```
|
||||
|
||||
…or restart `hermes dashboard`.
|
||||
|
||||
#### Plugin load lifecycle
|
||||
|
||||
1. Dashboard loads. `main.tsx` exposes the SDK on `window.__HERMES_PLUGIN_SDK__` and the registry on `window.__HERMES_PLUGINS__`.
|
||||
2. `App.tsx` calls `usePlugins()` → fetches `GET /api/dashboard/plugins`.
|
||||
3. For each manifest: CSS `<link>` is injected (if declared), then a `<script>` tag loads the JS bundle.
|
||||
4. The plugin's IIFE runs and calls `window.__HERMES_PLUGINS__.register(name, Component)` — and optionally `.registerSlot(name, slot, Component)` for each slot.
|
||||
5. The dashboard resolves the registered component against the manifest, adds the tab to navigation (unless `hidden`), and mounts the component as a route.
|
||||
|
||||
Plugins have up to **2 seconds** after their script loads to call `register()`. After that the dashboard stops waiting and finishes initial render. If a plugin later registers, it still appears — the nav is reactive.
|
||||
|
||||
If a plugin's script fails to load (404, syntax error, exception during IIFE), the dashboard logs a warning to the browser console and continues without it.
|
||||
|
||||
---
|
||||
|
||||
## Combined theme + plugin demo
|
||||
|
||||
The repo ships `plugins/strike-freedom-cockpit/` as a complete reskin demo. It pairs a theme YAML with a slot-only plugin to produce a cockpit-style HUD without forking the dashboard.
|
||||
|
||||
**What it demonstrates:**
|
||||
|
||||
- A full theme using palette, typography, `fontUrl`, `layoutVariant: cockpit`, `assets`, `componentStyles` (notched card corners, gradient backgrounds), `colorOverrides`, and `customCSS` (scanline overlay).
|
||||
- A slot-only plugin (`tab.hidden: true`) that registers into three slots:
|
||||
- `sidebar` — an MS-STATUS panel with live telemetry bars driven by `SDK.api.getStatus()`.
|
||||
- `header-left` — a faction crest that reads `--theme-asset-crest` from the active theme.
|
||||
- `footer-right` — a custom tagline replacing the default org line.
|
||||
- The plugin reads theme-supplied artwork via CSS vars, so swapping themes changes the hero/crest without plugin code changes.
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
# Theme
|
||||
cp plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
|
||||
~/.hermes/dashboard-themes/
|
||||
|
||||
# Plugin
|
||||
cp -r plugins/strike-freedom-cockpit ~/.hermes/plugins/
|
||||
```
|
||||
|
||||
Open the dashboard, pick **Strike Freedom** from the theme switcher. The cockpit sidebar appears, the crest shows in the header, the tagline replaces the footer. Switch back to **Hermes Teal** and the plugin remains installed but invisible (the `sidebar` slot only renders under the `cockpit` layout variant).
|
||||
|
||||
Read the plugin source (`plugins/strike-freedom-cockpit/dashboard/dist/index.js`) to see how it reads CSS vars, guards against older dashboards without slot support, and registers three slots from one bundle.
|
||||
|
||||
---
|
||||
|
||||
## API reference
|
||||
|
||||
### Theme endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/dashboard/themes` | GET | List available themes + active name. Built-ins return `{name, label, description}`; user themes also include a `definition` field with the full normalised theme object. |
|
||||
| `/api/dashboard/theme` | PUT | Set active theme. Body: `{"name": "midnight"}`. Persists to `config.yaml` under `dashboard.theme`. |
|
||||
|
||||
### Plugin endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/dashboard/plugins` | GET | List discovered plugins (with manifests, minus internal fields). |
|
||||
| `/api/dashboard/plugins/rescan` | GET | Force re-scan the plugin directories without restarting. |
|
||||
| `/dashboard-plugins/<name>/<path>` | GET | Serve static assets from a plugin's `dashboard/` directory. Path traversal is blocked. |
|
||||
| `/api/plugins/<name>/*` | * | Plugin-registered backend routes. |
|
||||
|
||||
### SDK on `window`
|
||||
|
||||
| Global | Type | Provider |
|
||||
|--------|------|----------|
|
||||
| `window.__HERMES_PLUGIN_SDK__` | object | `registry.ts` — React, hooks, UI components, API client, utils. |
|
||||
| `window.__HERMES_PLUGINS__.register(name, Component)` | function | Register a plugin's main component. |
|
||||
| `window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)` | function | Register into a named shell slot. |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**My theme doesn't appear in the picker.**
|
||||
Check that the file is in `~/.hermes/dashboard-themes/` and ends in `.yaml` or `.yml`. Refresh the page. Run `curl http://127.0.0.1:9119/api/dashboard/themes` — your theme should be in the response. If the YAML has a parse error, the dashboard logs to `errors.log` under `~/.hermes/logs/`.
|
||||
|
||||
**My plugin's tab doesn't show up.**
|
||||
1. Check the manifest is at `~/.hermes/plugins/<name>/dashboard/manifest.json` (note the `dashboard/` subdirectory).
|
||||
2. `curl http://127.0.0.1:9119/api/dashboard/plugins/rescan` to force re-discovery.
|
||||
3. Open browser dev tools → Network — confirm `manifest.json`, `index.js`, and any CSS loaded without 404s.
|
||||
4. Open browser dev tools → Console — look for errors during the IIFE or `window.__HERMES_PLUGINS__ is undefined` (indicates the SDK didn't initialize, usually a React render crash earlier).
|
||||
5. Verify your bundle calls `window.__HERMES_PLUGINS__.register(...)` with the **same name** as `manifest.json:name`.
|
||||
|
||||
**Slot-registered components don't render.**
|
||||
The `sidebar` slot only renders when the active theme has `layoutVariant: cockpit`. Other slots always render. If you're registering into a slot with no hits, add `console.log` inside `registerSlot` to confirm the plugin bundle ran at all.
|
||||
|
||||
**Plugin backend routes return 404.**
|
||||
1. Confirm the manifest has `"api": "plugin_api.py"` pointing to an existing file inside `dashboard/`.
|
||||
2. Restart `hermes dashboard` — plugin API routes are mounted once at startup, **not** on rescan.
|
||||
3. Check that `plugin_api.py` exports a module-level `router = APIRouter()`. Other export names are not picked up.
|
||||
4. Tail `~/.hermes/logs/errors.log` for `Failed to load plugin <name> API routes` — import errors are logged there.
|
||||
|
||||
**Theme change drops my color overrides.**
|
||||
`colorOverrides` are scoped to the active theme and cleared on theme switch — that's by design. If you want overrides that persist, put them in your theme's YAML, not in the live switcher.
|
||||
|
||||
**Theme customCSS gets truncated.**
|
||||
The `customCSS` block is capped at 32 KiB per theme. Split large stylesheets across multiple themes, or switch to a plugin that injects a full stylesheet via its `css` field (no size cap).
|
||||
|
||||
**I want to ship a plugin on PyPI.**
|
||||
Dashboard plugins are installed by directory layout, not by pip entry point. The cleanest distribution path today is a git repo the user clones into `~/.hermes/plugins/`. A pip-based installer for dashboard plugins is not currently wired up.
|
||||
|
|
@ -321,274 +321,27 @@ The frontend is built with React 19, TypeScript, Tailwind CSS v4, and shadcn/ui-
|
|||
|
||||
When you run `hermes update`, the web frontend is automatically rebuilt if `npm` is available. This keeps the dashboard in sync with code updates. If `npm` isn't installed, the update skips the frontend build and `hermes dashboard` will build it on first launch.
|
||||
|
||||
## Themes
|
||||
## Themes & plugins
|
||||
|
||||
Themes control the dashboard's visual presentation across three layers:
|
||||
The dashboard ships with six built-in themes and can be extended with user-defined themes, plugin tabs, and backend API routes — all drop-in, no repo clone needed.
|
||||
|
||||
- **Palette** — colors (background, text, accents, warm glow, noise)
|
||||
- **Typography** — font families, base size, line height, letter spacing
|
||||
- **Layout** — corner radius and density (spacing multiplier)
|
||||
**Switch themes live** from the header bar — click the palette icon next to the language switcher. Selection persists to `config.yaml` under `dashboard.theme` and is restored on page load.
|
||||
|
||||
Switch themes live from the header bar — click the palette icon next to the language switcher. Selection persists to `config.yaml` under `dashboard.theme` and is restored on page load.
|
||||
Built-in themes:
|
||||
|
||||
### Built-in themes
|
||||
| Theme | Character |
|
||||
|-------|-----------|
|
||||
| **Hermes Teal** (`default`) | Dark teal + cream, system fonts, comfortable spacing |
|
||||
| **Midnight** (`midnight`) | Deep blue-violet, Inter + JetBrains Mono |
|
||||
| **Ember** (`ember`) | Warm crimson + bronze, Spectral serif + IBM Plex Mono |
|
||||
| **Mono** (`mono`) | Grayscale, IBM Plex, compact |
|
||||
| **Cyberpunk** (`cyberpunk`) | Neon green on black, Share Tech Mono |
|
||||
| **Rosé** (`rose`) | Pink + ivory, Fraunces serif, spacious |
|
||||
|
||||
Each built-in ships its own palette, typography, and layout — switching produces visible changes beyond color alone.
|
||||
To build your own theme, add a plugin tab, inject into shell slots, or expose plugin-specific REST endpoints, see **[Extending the Dashboard](./extending-the-dashboard)** — the complete guide covers:
|
||||
|
||||
| Theme | Palette | Typography | Layout |
|
||||
|-------|---------|------------|--------|
|
||||
| **Hermes Teal** (`default`) | Dark teal + cream | System stack, 15px | 0.5rem radius, comfortable |
|
||||
| **Midnight** (`midnight`) | Deep blue-violet | Inter + JetBrains Mono, 14px | 0.75rem radius, comfortable |
|
||||
| **Ember** (`ember`) | Warm crimson / bronze | Spectral (serif) + IBM Plex Mono, 15px | 0.25rem radius, comfortable |
|
||||
| **Mono** (`mono`) | Grayscale | IBM Plex Sans + IBM Plex Mono, 13px | 0 radius, compact |
|
||||
| **Cyberpunk** (`cyberpunk`) | Neon green on black | Share Tech Mono everywhere, 14px | 0 radius, compact |
|
||||
| **Rosé** (`rose`) | Pink and ivory | Fraunces (serif) + DM Mono, 16px | 1rem radius, spacious |
|
||||
|
||||
Themes that reference Google Fonts (everything except Hermes Teal) load the stylesheet on demand — the first time you switch to them, a `<link>` tag is injected into `<head>`.
|
||||
|
||||
### Custom themes
|
||||
|
||||
Drop a YAML file in `~/.hermes/dashboard-themes/` and it appears in the picker automatically. The file can be as minimal as a name plus the fields you want to override — every missing field inherits a sane default.
|
||||
|
||||
Minimal example (colors only, bare hex shorthand):
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/dashboard-themes/neon.yaml
|
||||
name: neon
|
||||
label: Neon
|
||||
description: Pure magenta on black
|
||||
colors:
|
||||
background: "#000000"
|
||||
midground: "#ff00ff"
|
||||
```
|
||||
|
||||
Full example (every knob):
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/dashboard-themes/ocean.yaml
|
||||
name: ocean
|
||||
label: Ocean Deep
|
||||
description: Deep sea blues with coral accents
|
||||
|
||||
palette:
|
||||
background:
|
||||
hex: "#0a1628"
|
||||
alpha: 1.0
|
||||
midground:
|
||||
hex: "#a8d0ff"
|
||||
alpha: 1.0
|
||||
foreground:
|
||||
hex: "#ffffff"
|
||||
alpha: 0.0
|
||||
warmGlow: "rgba(255, 107, 107, 0.35)"
|
||||
noiseOpacity: 0.7
|
||||
|
||||
typography:
|
||||
fontSans: "Poppins, system-ui, sans-serif"
|
||||
fontMono: "Fira Code, ui-monospace, monospace"
|
||||
fontDisplay: "Poppins, system-ui, sans-serif" # optional, falls back to fontSans
|
||||
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
|
||||
baseSize: "15px"
|
||||
lineHeight: "1.6"
|
||||
letterSpacing: "-0.003em"
|
||||
|
||||
layout:
|
||||
radius: "0.75rem" # 0 | 0.25rem | 0.5rem | 0.75rem | 1rem | any length
|
||||
density: comfortable # compact | comfortable | spacious
|
||||
|
||||
# Optional — pin individual shadcn tokens that would otherwise derive from
|
||||
# the palette. Any key listed here wins over the palette cascade.
|
||||
colorOverrides:
|
||||
destructive: "#ff6b6b"
|
||||
ring: "#ff6b6b"
|
||||
```
|
||||
|
||||
Refresh the dashboard after creating the file.
|
||||
|
||||
### Palette model
|
||||
|
||||
The palette is a 3-layer triplet — **background**, **midground**, **foreground** — plus a warm-glow rgba() string and a noise-opacity multiplier. Every shadcn token (card, muted, border, primary, popover, etc.) is derived from this triplet via CSS `color-mix()` in the dashboard's stylesheet, so overriding three colors cascades into the whole UI.
|
||||
|
||||
- `background` — deepest canvas color (typically near-black). The page background and card fill come from this.
|
||||
- `midground` — primary text and accent. Most UI chrome reads this.
|
||||
- `foreground` — top-layer highlight. In the default theme this is white at alpha 0 (invisible); themes that want a bright accent on top can raise its alpha.
|
||||
- `warmGlow` — rgba() vignette color used by the ambient backdrop.
|
||||
- `noiseOpacity` — 0–1.2 multiplier on the grain overlay. Lower = softer, higher = grittier.
|
||||
|
||||
Each layer accepts `{hex, alpha}` or a bare hex string (alpha defaults to 1.0).
|
||||
|
||||
### Typography model
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `fontSans` | string | CSS font-family stack for body copy (applied to `html`, `body`) |
|
||||
| `fontMono` | string | CSS font-family stack for code blocks, `<code>`, `.font-mono` utilities, dense readouts |
|
||||
| `fontDisplay` | string | Optional heading/display font stack. Falls back to `fontSans` |
|
||||
| `fontUrl` | string | Optional external stylesheet URL. Injected as `<link rel="stylesheet">` in `<head>` on theme switch. Same URL is never injected twice. Works with Google Fonts, Bunny Fonts, self-hosted `@font-face` sheets, anything you can link |
|
||||
| `baseSize` | string | Root font size — controls the rem scale for the whole dashboard. Example: `"14px"`, `"16px"` |
|
||||
| `lineHeight` | string | Default line-height, e.g. `"1.5"`, `"1.65"` |
|
||||
| `letterSpacing` | string | Default letter-spacing, e.g. `"0"`, `"0.01em"`, `"-0.01em"` |
|
||||
|
||||
### Layout model
|
||||
|
||||
| Key | Values | Description |
|
||||
|-----|--------|-------------|
|
||||
| `radius` | any CSS length | Corner-radius token. Cascades into `--radius-sm/md/lg/xl` so every rounded element shifts together. |
|
||||
| `density` | `compact` \| `comfortable` \| `spacious` | Spacing multiplier. Compact = 0.85×, comfortable = 1.0× (default), spacious = 1.2×. Scales Tailwind's base spacing, so padding, gap, and space-between utilities all shift proportionally. |
|
||||
|
||||
### Color overrides (optional)
|
||||
|
||||
Most themes won't need this — the 3-layer palette derives every shadcn token. But if you want a specific accent that the derivation won't produce (a softer destructive red for a pastel theme, a specific success green for a brand), pin individual tokens here.
|
||||
|
||||
Supported keys: `card`, `cardForeground`, `popover`, `popoverForeground`, `primary`, `primaryForeground`, `secondary`, `secondaryForeground`, `muted`, `mutedForeground`, `accent`, `accentForeground`, `destructive`, `destructiveForeground`, `success`, `warning`, `border`, `input`, `ring`.
|
||||
|
||||
Any key set here overrides the derived value for the active theme only — switching to another theme clears the overrides.
|
||||
|
||||
### Layout variants
|
||||
|
||||
`layoutVariant` selects the overall shell layout. Defaults to `standard`.
|
||||
|
||||
| Variant | Behaviour |
|
||||
|---------|-----------|
|
||||
| `standard` | Single column, 1600px max-width (default) |
|
||||
| `cockpit` | Left sidebar rail (260px) + main content. Populated by plugins via the `sidebar` slot |
|
||||
| `tiled` | Drops the max-width clamp so pages can use the full viewport |
|
||||
|
||||
```yaml
|
||||
layoutVariant: cockpit
|
||||
```
|
||||
|
||||
The current variant is exposed as `document.documentElement.dataset.layoutVariant` so custom CSS can target it via `:root[data-layout-variant="cockpit"]`.
|
||||
|
||||
### Theme assets
|
||||
|
||||
Ship artwork URLs with a theme. Each named slot becomes a CSS var (`--theme-asset-<name>`) that plugins and the built-in shell read; the `bg` slot is automatically wired into the backdrop.
|
||||
|
||||
```yaml
|
||||
assets:
|
||||
bg: "https://example.com/hero-bg.jpg" # full-viewport background
|
||||
hero: "/my-images/strike-freedom.png" # for plugin sidebars
|
||||
crest: "/my-images/crest.svg" # for header slot plugins
|
||||
logo: "/my-images/logo.png"
|
||||
sidebar: "/my-images/rail.png"
|
||||
header: "/my-images/header-art.png"
|
||||
custom:
|
||||
scanLines: "/my-images/scanlines.png" # → --theme-asset-custom-scanLines
|
||||
```
|
||||
|
||||
Values accept bare URLs (wrapped in `url(...)` automatically), pre-wrapped `url(...)`/`linear-gradient(...)`/`radial-gradient(...)` expressions, and `none`.
|
||||
|
||||
### Component chrome overrides
|
||||
|
||||
Themes can restyle individual shell components without writing CSS selectors via the `componentStyles` block. Each bucket's entries become CSS vars (`--component-<bucket>-<kebab-property>`) that the shell's shared components read — so `card:` overrides apply to every `<Card>`, `header:` to the app bar, etc.
|
||||
|
||||
```yaml
|
||||
componentStyles:
|
||||
card:
|
||||
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
|
||||
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
|
||||
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
|
||||
header:
|
||||
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
|
||||
tab:
|
||||
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
|
||||
sidebar: {...}
|
||||
backdrop: {...}
|
||||
footer: {...}
|
||||
progress: {...}
|
||||
badge: {...}
|
||||
page: {...}
|
||||
```
|
||||
|
||||
Supported buckets: `card`, `header`, `footer`, `sidebar`, `tab`, `progress`, `badge`, `backdrop`, `page`. Property names use camelCase (`clipPath`) and are emitted as kebab (`clip-path`). Values are plain CSS strings — anything CSS accepts (`clip-path`, `border-image`, `background`, `box-shadow`, animations, etc.).
|
||||
|
||||
### Custom CSS
|
||||
|
||||
For selector-level chrome that doesn't fit `componentStyles` — pseudo-elements, animations, media queries, theme-scoped overrides — drop raw CSS into the `customCSS` field:
|
||||
|
||||
```yaml
|
||||
customCSS: |
|
||||
:root[data-layout-variant="cockpit"] body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
transparent 0px, transparent 2px,
|
||||
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
```
|
||||
|
||||
The CSS is injected as a single scoped `<style data-hermes-theme-css>` tag on theme apply and cleaned up on theme switch. Capped at 32 KiB per theme.
|
||||
|
||||
## Dashboard plugins
|
||||
|
||||
Plugins live in `~/.hermes/plugins/<name>/dashboard/` (user) or repo `plugins/<name>/dashboard/` (bundled). Each ships a `manifest.json` plus a plain JS bundle that uses the plugin SDK exposed on `window.__HERMES_PLUGIN_SDK__`.
|
||||
|
||||
### Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"label": "My Plugin",
|
||||
"icon": "Sparkles",
|
||||
"version": "1.0.0",
|
||||
"tab": {
|
||||
"path": "/my-plugin",
|
||||
"position": "after:skills",
|
||||
"override": "/",
|
||||
"hidden": false
|
||||
},
|
||||
"slots": ["sidebar", "header-left"],
|
||||
"entry": "dist/index.js",
|
||||
"css": "dist/index.css",
|
||||
"api": "api.py"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `tab.path` | Route path the plugin component renders at |
|
||||
| `tab.position` | `end`, `after:<tab>`, or `before:<tab>` |
|
||||
| `tab.override` | When set to a built-in path (`/`, `/sessions`, etc.), this plugin replaces that page instead of adding a new tab |
|
||||
| `tab.hidden` | When true, register component + slots but skip the nav entry. Used by slot-only plugins |
|
||||
| `slots` | Shell slots this plugin populates (documentation aid; actual registration happens from the JS bundle) |
|
||||
|
||||
### Shell slots
|
||||
|
||||
Plugins inject components into named shell locations by calling `window.__HERMES_PLUGINS__.registerSlot(pluginName, slotName, Component)`. Multiple plugins can populate the same slot — they render stacked in registration order.
|
||||
|
||||
| Slot | Location |
|
||||
|------|----------|
|
||||
| `backdrop` | Inside the backdrop layer stack |
|
||||
| `header-left` | Before the Hermes brand in the top bar |
|
||||
| `header-right` | Before the theme/language switchers |
|
||||
| `header-banner` | Full-width strip below the nav |
|
||||
| `sidebar` | Cockpit sidebar rail (only rendered when `layoutVariant === "cockpit"`) |
|
||||
| `pre-main` | Above the route outlet |
|
||||
| `post-main` | Below the route outlet |
|
||||
| `footer-left` / `footer-right` | Footer cell content (replaces default) |
|
||||
| `overlay` | Fixed-position layer above everything else |
|
||||
|
||||
### Plugin SDK
|
||||
|
||||
Exposed on `window.__HERMES_PLUGIN_SDK__`:
|
||||
|
||||
- `React` + `hooks` (useState, useEffect, useCallback, useMemo, useRef, useContext, createContext)
|
||||
- `components` — Card, Badge, Button, Input, Label, Select, Separator, Tabs, **PluginSlot**
|
||||
- `api` — Hermes API client, plus raw `fetchJSON`
|
||||
- `utils` — `cn()`, `timeAgo()`, `isoTimeAgo()`
|
||||
- `useI18n` — i18n hook for multi-language plugins
|
||||
|
||||
### Demo: Strike Freedom Cockpit
|
||||
|
||||
`plugins/strike-freedom-cockpit/` ships a complete skin demo showing every extension point — cockpit layout variant, theme-supplied hero/crest assets, notched card corners via `componentStyles`, scanlines via `customCSS`, and a slot-only plugin that populates the sidebar, header, and footer. Copy the theme YAML into `~/.hermes/dashboard-themes/` and the plugin directory into `~/.hermes/plugins/` to try it.
|
||||
|
||||
### Theme API
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/dashboard/themes` | GET | List available themes + active name. Built-ins return `{name, label, description}`; user themes also include a `definition` field with the full normalised theme object. |
|
||||
| `/api/dashboard/theme` | PUT | Set active theme. Body: `{"name": "midnight"}` |
|
||||
- Theme YAML schema — palette, typography, layout, assets, componentStyles, colorOverrides, customCSS
|
||||
- Layout variants — `standard`, `cockpit`, `tiled`
|
||||
- Plugin manifest, SDK, shell slots, backend FastAPI routes
|
||||
- A full combined theme-plus-plugin walkthrough (Strike Freedom cockpit demo)
|
||||
- Discovery, reload, and troubleshooting
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue