mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
docs(desktop): hermes-desktop-plugins skill + starter template
This commit is contained in:
parent
aff205dcc2
commit
a57b33782e
2 changed files with 209 additions and 0 deletions
136
skills/hermes-desktop-plugins/SKILL.md
Normal file
136
skills/hermes-desktop-plugins/SKILL.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
name: hermes-desktop-plugins
|
||||
description: Write desktop app plugins that add UI panes and commands.
|
||||
version: 1.0.0
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [desktop, plugins, ui, extension]
|
||||
category: productivity
|
||||
related_skills: []
|
||||
---
|
||||
|
||||
# Hermes Desktop Plugins Skill
|
||||
|
||||
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
|
||||
|
||||
1. 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`.
|
||||
2. 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**.)
|
||||
3. 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.
|
||||
- `host.request(method, params)` — gateway JSON-RPC (sessions, config,
|
||||
skills, cron — everything the app uses).
|
||||
- `host.onEvent(type, fn)` — live gateway events (`'*'` for all). Returns a
|
||||
disposer.
|
||||
- `host.notify({ kind, message })`, `host.navigate(path)`, `host.logs(...)`,
|
||||
`host.status()`, `haptic('tap')`.
|
||||
- `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
|
||||
|
||||
1. Pick a short kebab-case `id`; the folder name must match.
|
||||
2. Start from `templates/plugin.js`; keep the default export shape
|
||||
(`{ id, name, register(ctx) }`).
|
||||
3. 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.
|
||||
4. Fetch data with `host.request` and/or subscribe with `host.onEvent`;
|
||||
never poll faster than a few seconds.
|
||||
5. 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 <name> 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.
|
||||
73
skills/hermes-desktop-plugins/templates/plugin.js
Normal file
73
skills/hermes-desktop-plugins/templates/plugin.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Hermes desktop plugin template. Save as:
|
||||
* <hermes home>/desktop-plugins/<id>/plugin.js (folder name == id)
|
||||
* where <hermes home> is ~/.hermes by default, or ~/.hermes/profiles/<name>
|
||||
* when running a named profile (`hermes -p <name>`). Run `hermes doctor` (or
|
||||
* check the app's Settings → Plugins folder path) if unsure which is active.
|
||||
* Then run "Reload desktop plugins" from ⌘K in the desktop app.
|
||||
*
|
||||
* Plain ESM, loaded uncompiled — UI is jsx() calls, not JSX syntax.
|
||||
* Only these imports resolve: @hermes/plugin-sdk, react, react/jsx-runtime.
|
||||
*/
|
||||
|
||||
import { cn, haptic, host, Tip, useValue } from '@hermes/plugin-sdk'
|
||||
import { jsx, jsxs } from 'react/jsx-runtime'
|
||||
|
||||
function MyPane() {
|
||||
const gateway = useValue(host.state.gateway)
|
||||
|
||||
return jsxs('div', {
|
||||
className: 'flex h-full flex-col gap-2 p-3 text-sm',
|
||||
children: [
|
||||
jsx('div', { className: 'font-medium', children: 'My Plugin Pane' }),
|
||||
jsx('div', {
|
||||
className: 'text-(--ui-text-tertiary)',
|
||||
children: `gateway: ${gateway}`
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function MyChip() {
|
||||
return jsx(Tip, {
|
||||
label: 'My plugin — click me',
|
||||
children: jsx('button', {
|
||||
className: cn(
|
||||
'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] transition-colors',
|
||||
'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
),
|
||||
type: 'button',
|
||||
onClick: () => {
|
||||
haptic('tap')
|
||||
host.notify({ kind: 'info', message: 'Hello from my plugin!' })
|
||||
},
|
||||
children: 'my-plugin'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
id: 'my-plugin', // must match the folder name
|
||||
name: 'My Plugin',
|
||||
register(ctx) {
|
||||
// A layout pane — auto-placed by the placement hint; user can drag it.
|
||||
// To land on a specific edge instead of stacking, add a dock gesture,
|
||||
// e.g. below the conversation:
|
||||
// data: { placement: 'bottom', dock: { pane: 'workspace', pos: 'bottom' }, height: '200px' }
|
||||
ctx.register({
|
||||
id: 'pane',
|
||||
area: 'panes',
|
||||
title: 'my plugin',
|
||||
data: { placement: 'right', width: '237px' },
|
||||
render: () => jsx(MyPane, {})
|
||||
})
|
||||
|
||||
// A statusbar chip.
|
||||
ctx.register({
|
||||
id: 'chip',
|
||||
area: 'statusBar.right',
|
||||
order: 130,
|
||||
render: () => jsx(MyChip, {})
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue