hermes-agent/plugins/example-dashboard/dashboard/dist/index.js
Teknium 01214a7f73 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 04:10:06 -07:00

94 lines
4.7 KiB
JavaScript

/**
* 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);
})();