Write desktop app plugins that add UI panes and commands.
Installation
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Write plugins for the Hermes desktop app: statusbar items, layout panes,
command-palette commands, keybinds, routes, and themes. A plugin is a single
plain-JavaScript ESM file the app loads at runtime — no build step, no repo
changes. This skill does not cover backend plugins (~/.hermes/plugins/);
those are Python and documented separately.
When to Use
The user asks for a new desktop UI element (a pane, a statusbar widget, a
dashboard, a command) without modifying the app itself.
You want to surface data you compute (via gateway RPC) inside the app.
Prerequisites
The Hermes desktop app (it loads plugins; the CLI/gateway alone does not).
Write access to $HERMES_HOME/desktop-plugins/ (usually
~/.hermes/desktop-plugins/).
How to Run
Create $HERMES_HOME/desktop-plugins/<name>/plugin.js from
templates/plugin.js (relative to this skill directory) — that's
~/.hermes/... by default, or ~/.hermes/profiles/<profile>/... under a
named profile. Keep <name> equal to the plugin id.
The desktop app watches that directory: the plugin loads within a few
seconds of the file landing, and every later save hot-reloads it in
place. No reload step. (Fallback if it doesn't appear: ⌘K →
Reload desktop plugins.)
If loading fails the app shows a toast naming the error — fix the file
and save again.
Quick Reference
The ONLY import surface is @hermes/plugin-sdk (plus react /
react/jsx-runtime, which resolve to the app's own React — write UI with
jsx() calls, not JSX syntax; the file is not compiled).
host.state.* — readonly reactive atoms: activeSessionId, cwd,
gateway, model, profile, viewport. Read with .get() in handlers,
useValue(atom) in components.
ctx.register({ id, area, order?, render?, data? }) — contribute UI.
Key areas: 'statusBar.right'/'statusBar.left' (chips),
'panes' (layout zones — set title and
data: { placement, dock?, width?, height? }; the pane auto-joins a
matching zone), PALETTE_AREA (⌘K commands), KEYBINDS_AREA (rebindable
actions).
Pane placement: placement: 'left'|'right'|'bottom'|'main' is the
semantic role — the pane stacks (tabs) with existing panes of that role.
To land on a specific EDGE instead, add dock: { pane, pos } — the same
gesture as dragging onto a pane's drop chip. pane is any pane id
(workspace is the main thread; also sessions, terminal, files,
review, logs), pos is 'top'|'bottom'|'left'|'right'|'center'.
E.g. "below the conversation" = dock: { pane: 'workspace', pos: 'bottom' }
— declare a height (e.g. '200px') so it doesn't take half the zone.
Full PAGES: register area: ROUTES_AREA with data: { path: '/my-page' }
and a render — the page mounts in the workspace (main) pane like any
built-in view. Make it reachable with a sidebar nav row:
ctx.register({ id: 'nav', area: SIDEBAR_NAV_AREA, data: { path: '/my-page', label: 'My Page', codicon: 'project' } })
(renders below Artifacts, lights up at the route) — and/or a
PALETTE_AREA command calling host.navigate('/my-page').
ctx.storage.get/set/remove — persistence namespaced to your plugin.
Users manage plugins in Settings → Plugins (enable/disable live, reveal
folder). A disabled plugin stays disabled across restarts — don't fight
it; the user turned you off.
UI: the app's design language, importable directly — Button, Input,
Textarea, Select*, Switch, Checkbox, SegmentedControl, Tabs*,
Dialog*, ConfirmDialog, DropdownMenu*, ContextMenu*, Popover*,
Tip/Tooltip*, Badge, Kbd/KbdGroup, SearchField, ScrollArea,
Separator, Skeleton, GlyphSpinner, EmptyState, ErrorState,
CopyButton, StatusDot, LogView, Codicon, DecodeText, plus cn
and icons.*. Prefer these over hand-rolled elements so the plugin looks
native; style with theme vars, never hardcoded colors.
Procedure
Pick a short kebab-case id; the folder name must match.
Start from templates/plugin.js; keep the default export shape
({ id, name, register(ctx) }).
For a pane, register area: 'panes' with a placement hint and a
render returning your component — the app places it into a sensible
zone automatically; the user can drag it anywhere afterwards.
Fetch data with host.request and/or subscribe with host.onEvent;
never poll faster than a few seconds.
Write the file with your file tools, then ask the user to run
Reload desktop plugins from ⌘K.
Pitfalls
NEVER hardcode colors or backgrounds (#000, black, rgb(...)). Panes
already sit on the app's editor background — leave the background alone
and use theme variables for everything else: var(--ui-text-secondary),
var(--ui-text-quaternary), var(--ui-stroke-secondary),
var(--ui-accent). For canvas drawing, resolve them once with
getComputedStyle(canvas).getPropertyValue('--ui-accent').
Reference only what you imported — a component you forgot to import
(e.g. StatusDot) is a ReferenceError at render. Double-check every
identifier in your jsx() calls appears in the import line.
Canvas panes MUST track their container with a ResizeObserver and
re-size the canvas (width/height attributes, not just CSS) — panes resize
constantly (sash drags, layout switches); a mount-time-only size leaves
blank space or blurry scaling.
JSX syntax will not parse — the file loads uncompiled. Use
jsx('div', { children: ... }) from react/jsx-runtime.
Do not import anything except @hermes/plugin-sdk, react, and
react/jsx-runtime; other specifiers fail to resolve.
Handlers must read state imperatively ($atom.get()), never from render
closures — rapid events will otherwise see stale values.
Keep components small; subscribe (useValue) only in the leaf that
renders the value.
Verification
The plugin's UI appears after Reload desktop plugins.
No error toast ("Plugin failed to load") appears; if it does, the
message names the failure — fix and reload.
For panes: the new zone is visible and draggable like any core pane.