mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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).
This commit is contained in:
parent
23a42635f0
commit
01214a7f73
11 changed files with 660 additions and 15 deletions
94
plugins/example-dashboard/dashboard/dist/index.js
vendored
Normal file
94
plugins/example-dashboard/dashboard/dist/index.js
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Example Dashboard Plugin
|
||||
*
|
||||
* Demonstrates how to build a dashboard plugin using the Hermes Plugin SDK.
|
||||
* No build step needed — this is a plain IIFE that uses globals from the SDK.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const SDK = window.__HERMES_PLUGIN_SDK__;
|
||||
const { React } = SDK;
|
||||
const { Card, CardHeader, CardTitle, CardContent, Badge, Button } = SDK.components;
|
||||
const { useState, useEffect } = SDK.hooks;
|
||||
const { cn } = SDK.utils;
|
||||
|
||||
function ExamplePage() {
|
||||
const [greeting, setGreeting] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
function fetchGreeting() {
|
||||
setLoading(true);
|
||||
SDK.fetchJSON("/api/plugins/example/hello")
|
||||
.then(function (data) { setGreeting(data.message); })
|
||||
.catch(function () { setGreeting("(backend not available)"); })
|
||||
.finally(function () { setLoading(false); });
|
||||
}
|
||||
|
||||
return React.createElement("div", { className: "flex flex-col gap-6" },
|
||||
// Header card
|
||||
React.createElement(Card, null,
|
||||
React.createElement(CardHeader, null,
|
||||
React.createElement("div", { className: "flex items-center gap-3" },
|
||||
React.createElement(CardTitle, { className: "text-lg" }, "Example Plugin"),
|
||||
React.createElement(Badge, { variant: "outline" }, "v1.0.0"),
|
||||
),
|
||||
),
|
||||
React.createElement(CardContent, { className: "flex flex-col gap-4" },
|
||||
React.createElement("p", { className: "text-sm text-muted-foreground" },
|
||||
"This is an example dashboard plugin. It demonstrates using the Plugin SDK to build ",
|
||||
"custom tabs with React components, connect to backend API routes, and integrate with ",
|
||||
"the existing Hermes UI system.",
|
||||
),
|
||||
React.createElement("div", { className: "flex items-center gap-3" },
|
||||
React.createElement(Button, {
|
||||
onClick: fetchGreeting,
|
||||
disabled: loading,
|
||||
className: cn(
|
||||
"inline-flex items-center gap-2 border border-border bg-background/40 px-4 py-2",
|
||||
"text-sm font-courier transition-colors hover:bg-foreground/10 cursor-pointer",
|
||||
),
|
||||
}, loading ? "Loading..." : "Call Backend API"),
|
||||
greeting && React.createElement("span", {
|
||||
className: "text-sm font-courier text-muted-foreground",
|
||||
}, greeting),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Info card about the SDK
|
||||
React.createElement(Card, null,
|
||||
React.createElement(CardHeader, null,
|
||||
React.createElement(CardTitle, { className: "text-base" }, "Plugin SDK Reference"),
|
||||
),
|
||||
React.createElement(CardContent, null,
|
||||
React.createElement("div", { className: "grid gap-3 text-sm" },
|
||||
React.createElement("div", { className: "flex flex-col gap-1 border border-border p-3" },
|
||||
React.createElement("span", { className: "font-medium" }, "window.__HERMES_PLUGIN_SDK__.React"),
|
||||
React.createElement("span", { className: "text-muted-foreground text-xs" }, "React instance — use instead of importing react"),
|
||||
),
|
||||
React.createElement("div", { className: "flex flex-col gap-1 border border-border p-3" },
|
||||
React.createElement("span", { className: "font-medium" }, "window.__HERMES_PLUGIN_SDK__.hooks"),
|
||||
React.createElement("span", { className: "text-muted-foreground text-xs" }, "useState, useEffect, useCallback, useMemo, useRef, useContext, createContext"),
|
||||
),
|
||||
React.createElement("div", { className: "flex flex-col gap-1 border border-border p-3" },
|
||||
React.createElement("span", { className: "font-medium" }, "window.__HERMES_PLUGIN_SDK__.components"),
|
||||
React.createElement("span", { className: "text-muted-foreground text-xs" }, "Card, Badge, Button, Input, Label, Select, Separator, Tabs, etc."),
|
||||
),
|
||||
React.createElement("div", { className: "flex flex-col gap-1 border border-border p-3" },
|
||||
React.createElement("span", { className: "font-medium" }, "window.__HERMES_PLUGIN_SDK__.api"),
|
||||
React.createElement("span", { className: "text-muted-foreground text-xs" }, "Hermes API client — getStatus(), getSessions(), etc."),
|
||||
),
|
||||
React.createElement("div", { className: "flex flex-col gap-1 border border-border p-3" },
|
||||
React.createElement("span", { className: "font-medium" }, "window.__HERMES_PLUGIN_SDK__.utils"),
|
||||
React.createElement("span", { className: "text-muted-foreground text-xs" }, "cn(), timeAgo(), isoTimeAgo()"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Register this plugin — the dashboard picks it up automatically.
|
||||
window.__HERMES_PLUGINS__.register("example", ExamplePage);
|
||||
})();
|
||||
13
plugins/example-dashboard/dashboard/manifest.json
Normal file
13
plugins/example-dashboard/dashboard/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "example",
|
||||
"label": "Example",
|
||||
"description": "Example dashboard plugin — demonstrates the plugin SDK",
|
||||
"icon": "Sparkles",
|
||||
"version": "1.0.0",
|
||||
"tab": {
|
||||
"path": "/example",
|
||||
"position": "after:skills"
|
||||
},
|
||||
"entry": "dist/index.js",
|
||||
"api": "plugin_api.py"
|
||||
}
|
||||
14
plugins/example-dashboard/dashboard/plugin_api.py
Normal file
14
plugins/example-dashboard/dashboard/plugin_api.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Example dashboard plugin — backend API routes.
|
||||
|
||||
Mounted at /api/plugins/example/ by the dashboard plugin system.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/hello")
|
||||
async def hello():
|
||||
"""Simple greeting endpoint to demonstrate plugin API routes."""
|
||||
return {"message": "Hello from the example plugin!", "plugin": "example", "version": "1.0.0"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue