@blackbelt-technology/dashboard-plugin-runtime
v0.5.3
Published
Plugin loader, slot registry, slot consumers, and context API for pi-dashboard
Keywords
Readme
@blackbelt-technology/dashboard-plugin-runtime
Plugin loader, slot registry, slot consumers, plugin context API, and Vite plugin for pi-dashboard.
Import paths
| Path | Contents |
|------|----------|
| @blackbelt-technology/dashboard-plugin-runtime | Slot consumers, registry types, barrel |
| @blackbelt-technology/dashboard-plugin-runtime/context | Client-side hooks (usePluginConfig, useAllSessions, etc.) |
| @blackbelt-technology/dashboard-plugin-runtime/server | Loader, ServerPluginContext, config validator |
| @blackbelt-technology/dashboard-plugin-runtime/vite-plugin | viteDashboardPluginsPlugin |
Plugins MUST only import from these public paths. Importing from packages/client/, packages/server/, or any other internal package is banned and will fail the lint suite.
Minimum manifest
Add a pi-dashboard-plugin field to your package's package.json:
{
"name": "@blackbelt-technology/my-feature-plugin",
"pi-dashboard-plugin": {
"id": "my-feature",
"displayName": "My Feature",
"priority": 100,
"client": "./dist/client/index.js",
"server": "./dist/server/index.js",
"claims": [
{ "slot": "session-card-badge", "component": "MyFeatureBadge" }
]
}
}Fields:
id— globally unique kebab-case id.priority— sort order for multi-contribution slots. Lower = rendered first. Default 1000; first-party = 100.client— path to the built client entry (exporting React components by name).server— optional path to the server entry (exportsdefault function registerPlugin(ctx: ServerPluginContext)).bridge— optional path to a pi-extension/bridge entry (auto-registered into~/.pi/agent/settings.jsonunderdashboard-<id>).configSchema— optional relative path to a JSON Schema 7 file for plugin config validation.claims— array of slot claims.
Slot claims
Each claim targets one slot:
{ "slot": "session-card-badge", "component": "MyBadge" }
{ "slot": "tool-renderer", "toolName": "MyTool", "component": "MyToolRenderer" }
{ "slot": "command-route", "command": "/myfeature", "component": "MyFeatureView" }
{ "slot": "anchored-popover", "trigger": "my-trigger-button", "component": "MyPopover" }
{ "slot": "settings-section", "component": "MySettings", "tab": "general" }settings-section tab field
Use tab to control which tab of the Settings page your section appears in:
| Value | Tab |
|-------|-----|
| general (default) | General |
| servers | Servers |
| packages | Packages |
| providers | Providers |
| security | Security |
| advanced | Advanced |
Client-side PluginContext API
Plugin client components receive props from the slot consumer. Hooks are available via the nearest PluginContextProvider:
import {
usePluginConfig,
useAllSessions,
useSessionState,
usePluginLogger,
usePluginSend,
usePluginRouter,
} from "@blackbelt-technology/dashboard-plugin-runtime/context";
function MyBadge({ session }) {
const config = usePluginConfig<{ enabled: boolean }>();
const logger = usePluginLogger(); // logs as [plugin:my-feature]
const send = usePluginSend();
// ...
}usePluginConfig<T>() is reactive — it re-renders when POST /api/config/plugins/<id> succeeds and the server broadcasts plugin_config_update.
You MUST call these hooks from within a slot contribution component (i.e. inside a CurrentPluginLayer). Calling them from outside throws a descriptive error.
Server-side ServerPluginContext API
Your server entry must export a default registerPlugin function:
// packages/my-feature-plugin/server/index.ts
import type { ServerPluginContext } from "@blackbelt-technology/dashboard-plugin-runtime/server";
export default async function registerPlugin(ctx: ServerPluginContext): Promise<void> {
ctx.fastify.get("/api/my-feature/status", async () => {
return { ok: true };
});
ctx.logger.info("my-feature plugin loaded");
}Available on ctx:
fastify— Fastify instance for REST routes.sessionManager/eventStore— read-only dashboard state.broadcastToSubscribers(msg)— send a WebSocket message to all subscribed browsers.registerPiHandler(type, handler)/registerBrowserHandler(type, handler)— hook into WebSocket message flows.getPluginConfig<T>()— read this plugin's config from~/.pi/dashboard/config.json#plugins.<id>.*.updatePluginConfig<T>(partial)— validate, merge, persist, and broadcastplugin_config_update.logger— namespaced logger ([plugin:<id>]).
Bridge auto-register
If your manifest declares bridge, the dashboard auto-registers it in ~/.pi/agent/settings.json under dashboardPluginBridges["dashboard-<id>"] on server startup. The dashboard removes the entry when the plugin is disabled.
The dashboard- key prefix is reserved. User-owned extension entries in packages[] are never touched.
Plugin config persistence
Plugin settings live at ~/.pi/dashboard/config.json#plugins.<id>.*.
{
"plugins": {
"my-feature": { "enabled": true, "pollInterval": 30 }
}
}If your manifest declares configSchema, the loader:
- Applies schema
defaultvalues on read. - Validates writes before persisting (rejects with
ValidationErroron schema violation).
Failure isolation rules
- A plugin throwing during manifest validation, server-side load, or client-side render does NOT crash the dashboard.
- Failures are reflected in
/api/health.plugins[]as{ loaded: false, error: "..." }. - Slot consumer error boundaries catch React render errors per-claim (not per-slot), so one plugin crashing does not suppress siblings.
Demo plugin
packages/demo-plugin/ is a private fixture package that exercises the runtime end-to-end. It is excluded from production builds (manifest declares fixture: true). Do not use it as a template for real plugins.
