mibyan dashboard) is built to be reskinned and extended without forking the codebase. Three layers are exposed:
- Themes — YAML files that repaint the dashboard’s palette, typography, layout, and per-component chrome. Drop a file in
~/.mibyan/dashboard-themes/; it appears in the theme switcher. - UI plugins — a directory with
manifest.json+ a JavaScript bundle that registers a tab, replaces a built-in page, augments one via page-scoped slots, or injects components into named shell slots. - 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.
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. If you want to reskin the terminal CLI (not the web dashboard), see Skins & Themes — the CLI skin system is unrelated to dashboard themes.
Not the desktop appThis page covers the web dashboard (
mibyan dashboard) plugin system — window.__mibyan_PLUGIN_SDK__, a manifest.json, and a pre-built JS bundle. The native desktop app (mibyan desktop) has its own, unrelated SDK — @mibyan/plugin-sdk, a single ESM file, no build step — documented at Desktop Plugin SDK. Only the backend plugin_api.py namespace (/api/plugins/<name>) is shared between them.How the pieces composeThemes 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 example
strike-freedom-cockpit demo (lives in the mibyan-example-plugins companion repo — see Combined theme + plugin demo for install steps) does exactly that.Table of contents
Themes
Themes are YAML files stored in~/.mibyan/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
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 CSScolor-mix(). Overriding three colors cascades into the whole UI.
Each layer accepts either
{hex: "#RRGGBB", alpha: 0.0–1.0} or a bare hex string (alpha defaults to 1.0).
Typography
Changing the font from the UI (no YAML)
The theme picker in the dashboard header has a Font section below the theme list. Pick any font there and it overrides the body font of whatever theme is active — the choice is independent of the theme and persists across theme switches (stored inconfig.yaml under dashboard.font). Choose
Theme default to clear the override and fall back to the active theme’s
own fontSans.
The picker offers a curated catalog (system stacks plus a set of Google-Fonts
families across sans / serif / mono). It deliberately does not accept a
free-text font URL — the font’s stylesheet is injected as a <link>, so the
catalog keeps the injected origins fixed. For a fully custom face, set
fontSans + fontUrl in a theme YAML as shown above. The theme’s fontMono
(code blocks, terminal) is always left untouched by the UI override.
Layout
Layout variants
layoutVariant picks the overall shell layout. Defaults to "standard" when absent.
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.
- Bare URLs — wrapped in
url(...)automatically. - Pre-wrapped
url(...),linear-gradient(...),radial-gradient(...)expressions — used as-is. "none"— explicit opt-out.
--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:
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.
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. UsecolorOverrides 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).
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:
<style data-mibyan-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.
Themes that reference Google Fonts (all except Mibyan 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:config.yaml under dashboard.theme and is restored on reload.
Plugins
A dashboard plugin is a directory with amanifest.json, a pre-built JS bundle, and optionally a CSS file and a Python file with FastAPI routes. Plugins live next to other Mibyan plugins in ~/.mibyan/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.__mibyan_PLUGIN_SDK__. This keeps plugin bundles tiny (typically a few KB) and avoids version conflicts.
Quick start — your first plugin
Create the directory structure:Directory layout
plugin.yaml+__init__.py— CLI/gateway plugin (see plugins page).dashboard/manifest.json+dashboard/dist/index.js— dashboard UI plugin.dashboard/plugin_api.py— dashboard backend routes.
Manifest reference
Available icons
Plugins use Lucide icon names. The dashboard maps these by name — unknown names silently fall back toPuzzle.
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 onwindow.__mibyan_PLUGIN_SDK__. Plugins should never import React directly.
Calling your plugin’s backend
fetchJSON injects the session auth token, surfaces errors as thrown exceptions, and parses JSON automatically.
Calling built-in Mibyan endpoints
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:Slot catalogue
Shell-wide slots (render anywhere in the app chrome):
Page-scoped slots (render only on the named built-in page — use these to inject widgets, cards, or toolbars into an existing page without overriding the whole route):
Example — add a banner card to the top of the Sessions page:
tab.hidden: true if your plugin only augments existing pages and doesn’t need a sidebar tab of its own.
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.
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).
Augmenting built-in pages (page-scoped slots)
Full replacement viatab.override is heavy — your plugin now owns the entire page, including any future updates we ship to it. Most of the time you just want to add a banner, card, or toolbar to an existing page. That’s what page-scoped slots are for.
Every built-in page exposes <page>:top and <page>:bottom slots rendered at the top and bottom of its content area. Your plugin populates one by calling registerSlot() — the built-in page keeps working normally, and your component renders alongside it.
Available slots: sessions:*, analytics:*, logs:*, cron:*, skills:*, config:*, env:*, docs:*, chat:* (each with :top and :bottom). See the full catalogue in Shell slots → Slot catalogue.
Minimal example — pin a banner to the top of the Sessions page:
tab.hidden: truekeeps the plugin out of the sidebar — it has no standalone page.- The
slotsmanifest field is documentation only. The actual binding happens in the JS bundle viaregisterSlot(). - Multiple plugins can claim the same page-scoped slot. They render stacked in registration order.
- Zero footprint when no plugin registers: the built-in page renders exactly as before.
example-dashboard in mibyan-example-plugins) ships a live demo that injects a banner into sessions:top — install it to see the pattern end-to-end.
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.
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 settingapi in the manifest. Create the file and export a router:
/api/plugins/<name>/, so the above becomes:
GET /api/plugins/my-plugin/dataPOST /api/plugins/my-plugin/action
401 before the plugin route runs, and requests to a disabled plugin’s routes are rejected at request time. Still, don’t expose the dashboard on a public interface with --host 0.0.0.0 if you run untrusted plugins — an authenticated session can reach their routes too.
Accessing Mibyan internals
Backend routes run inside the dashboard process, so they can import from the mibyan-agent codebase directly:Custom CSS per plugin
If your plugin needs styles beyond Tailwind classes and inlinestyle=, add a CSS file and reference it in the manifest:
<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:
--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 fordashboard/manifest.json:
Discovery results are cached per dashboard process. After adding a new plugin, either:
mibyan dashboard.
Plugin load lifecycle
- Dashboard loads.
main.tsxexposes the SDK onwindow.__mibyan_PLUGIN_SDK__and the registry onwindow.__mibyan_PLUGINS__. App.tsxcallsusePlugins()→ fetchesGET /api/dashboard/plugins.- For each manifest: CSS
<link>is injected (if declared), then a<script>tag loads the JS bundle. - The plugin’s IIFE runs and calls
window.__mibyan_PLUGINS__.register(name, Component)— and optionally.registerSlot(name, slot, Component)for each slot. - The dashboard resolves the registered component against the manifest, adds the tab to navigation (unless
hidden), and mounts the component as a route.
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
Thestrike-freedom-cockpit plugin (companion repo mibyan-example-plugins) is 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, andcustomCSS(scanline overlay). - A slot-only plugin (
tab.hidden: true) that registers into three slots:sidebar— an MS-STATUS panel with live telemetry bars driven bySDK.api.getStatus().header-left— a faction crest that reads--theme-asset-crestfrom 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.
sidebar slot only renders under the cockpit layout variant).
Read the plugin source (strike-freedom-cockpit/dashboard/dist/index.js in the companion repo) 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
Plugin endpoints
SDK on window
Troubleshooting
My theme doesn’t appear in the picker. Check that the file is in~/.mibyan/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 ~/.mibyan/logs/.
My plugin’s tab doesn’t show up.
- Check the manifest is at
~/.mibyan/plugins/<name>/dashboard/manifest.json(note thedashboard/subdirectory). curl http://127.0.0.1:9119/api/dashboard/plugins/rescanto force re-discovery.- Open browser dev tools → Network — confirm
manifest.json,index.js, and any CSS loaded without 404s. - Open browser dev tools → Console — look for errors during the IIFE or
window.__mibyan_PLUGINS__ is undefined(indicates the SDK didn’t initialize, usually a React render crash earlier). - Verify your bundle calls
window.__mibyan_PLUGINS__.register(...)with the same name asmanifest.json:name.
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.
- Confirm the manifest has
"api": "plugin_api.py"pointing to an existing file insidedashboard/. - Restart
mibyan dashboard— plugin API routes are mounted once at startup, not on rescan. - Check that
plugin_api.pyexports a module-levelrouter = APIRouter(). Other export names are not picked up. - Tail
~/.mibyan/logs/errors.logforFailed to load plugin <name> API routes— import errors are logged there.
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 ~/.mibyan/plugins/. A pip-based installer for dashboard plugins is not currently wired up.
