Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Estender o painel

O painel web do Hermes (hermes dashboard) foi feito para ser reestilizado e estendido sem fazer fork do código. Três camadas ficam expostas:

  1. Temas — arquivos YAML que repintam a paleta, a tipografia, o layout e o acabamento de cada componente do painel. Solte um arquivo em ~/.hermes/dashboard-themes/; ele aparece no seletor de temas.
  2. Plugins de UI — um diretório com manifest.json + um bundle JavaScript que registra uma aba, substitui uma página embutida, complementa uma delas por slots com escopo de página, ou injeta componentes em slots nomeados do shell.
  3. Plugins de backend — um arquivo Python dentro daquele diretório de plugin que expõe um router do FastAPI; as rotas são montadas sob /api/plugins/<nome>/ e chamadas a partir da UI do plugin.

As três são plug-and-play em tempo de execução: sem clonar o repositório, sem npm run build, sem aplicar patch no código do painel. Esta página é a referência canônica para as três.

Se você só quer usar o painel, veja Painel Web. Se você quer reestilizar a CLI de terminal (e não o painel web), veja Skins e Temas — o sistema de skins da CLI não tem relação com os temas do painel.

Não é o app desktop

Esta página cobre o sistema de plugins do painel web (hermes dashboard) — window.__HERMES_PLUGIN_SDK__, um manifest.json e um bundle JS pré-construído. O app desktop nativo (hermes desktop) tem o próprio SDK, sem relação com este — @hermes/plugin-sdk, um único arquivo ESM, sem etapa de build — documentado em SDK de Plugin do Desktop. Só o namespace de backend do plugin_api.py (/api/plugins/<nome>) é compartilhado entre os dois.

Como as peças se combinam

Temas e plugins são independentes, mas se potencializam. Um tema pode viver sozinho (só um arquivo YAML). Um plugin pode viver sozinho (só uma aba). Juntos, permitem montar uma reestilização visual completa com HUDs personalizados — a demo strike-freedom-cockpit (que vive no repositório companheiro hermes-example-plugins — veja Demo combinando tema + plugin para os passos de instalação) faz exatamente isso.


Sumário​


Temas​

Temas são arquivos YAML guardados em ~/.hermes/dashboard-themes/. O nome do arquivo não importa (o que o sistema usa é o campo name: do tema), mas a convenção é <nome>.yaml. Todo campo é opcional — chaves ausentes caem no tema embutido default, então um tema pode ser tão pequeno quanto uma única cor.

Início rápido — seu primeiro tema​

mkdir -p ~/.hermes/dashboard-themes
# ~/.hermes/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Magenta puro no preto

palette:
background: "#000000"
midground: "#ff00ff"

Recarregue o painel. Clique no ícone de paleta no cabeçalho e escolha Neon. O fundo fica preto, o texto e os destaques ficam magenta, e cada cor derivada (card, borda, apagado, ring etc.) é recalculada a partir dessa dupla de cores via color-mix() no CSS.

Essa é toda a entrada: um arquivo, duas cores. Tudo o que vem abaixo é refinamento opcional.

Paleta, tipografia, layout​

Esses três blocos são o coração de um tema. Cada um é independente — sobrescreva um e deixe os outros.

Paleta (3 camadas)​

A paleta é uma trinca de camadas de cor, mais uma cor de vinheta com brilho quente e um multiplicador de granulado. A cascata do sistema de design do painel deriva cada token compatível com shadcn (card, popover, muted, border, primary, destructive, ring etc.) dessa trinca, via color-mix() do CSS. Sobrescrever três cores repercute em toda a interface.

ChaveDescrição
palette.backgroundCor mais profunda da tela — geralmente quase preta. Comanda o fundo da página e o preenchimento dos cards.
palette.midgroundTexto principal e destaque. A maior parte do acabamento da interface lê isso (texto de primeiro plano, contorno de botões, anéis de foco).
palette.foregroundRealce da camada de cima. O tema padrão define isso como branco com alfa 0 (invisível); temas que queiram um destaque forte por cima podem aumentar o alfa.
palette.warmGlowString rgba(...) usada como cor de vinheta pelo <Backdrop />.
palette.noiseOpacityMultiplicador de 0 a 1,2 sobre a camada de granulado. Menor = mais suave, maior = mais áspero.

Cada camada aceita ou {hex: "#RRGGBB", alpha: 0.0–1.0} ou uma string hex simples (o alfa cai para 1.0).

palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # hex simples, alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # camada de cima invisível
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 0.7

Tipografia​

ChaveTipoDescrição
fontSansstringPilha CSS de font-family para o texto corrido (aplicada em html, body).
fontMonostringPilha CSS de font-family para blocos de código, <code> e utilitários .font-mono.
fontDisplaystringPilha opcional para títulos/display. Cai para fontSans.
fontUrlstringURL opcional de folha de estilo externa. Injetada como <link rel="stylesheet"> no <head> ao trocar de tema. A mesma URL nunca é injetada duas vezes. Funciona com Google Fonts, Bunny Fonts, folhas @font-face auto-hospedadas — qualquer coisa linkável.
baseSizestringTamanho de fonte raiz — controla a escala rem. Por exemplo, "14px", "16px".
lineHeightstringAltura de linha padrão. Por exemplo, "1.5", "1.65".
letterSpacingstringEspaçamento entre letras padrão. Por exemplo, "0", "0.01em", "-0.01em".
typography:
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
fontMono: '"Share Tech Mono", ui-monospace, monospace'
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
baseSize: "14px"
lineHeight: "1.5"
letterSpacing: "0.04em"
Trocar a fonte pela interface (sem YAML)​

O seletor de temas no cabeçalho do painel tem uma seção Font abaixo da lista de temas. Escolha qualquer fonte ali e ela sobrescreve a fonte de texto do tema ativo — a escolha é independente do tema e persiste entre trocas de tema (guardada no config.yaml sob dashboard.font). Escolha Theme default para limpar a sobrescrita e voltar ao fontSans do tema ativo.

O seletor oferece um catálogo curado (pilhas do sistema mais um conjunto de famílias do Google Fonts entre sans / serif / mono). Ele deliberadamente não aceita uma URL de fonte em texto livre — a folha de estilo da fonte é injetada como um <link>, então o catálogo mantém fixas as origens injetadas. Para um tipo totalmente personalizado, defina fontSans + fontUrl no YAML do tema, como mostrado acima. O fontMono do tema (blocos de código, terminal) nunca é tocado pela sobrescrita da interface.

Layout​

ChaveValoresDescrição
radiusqualquer comprimento CSS ("0", "0.25rem", "0.5rem", "1rem", ...)Token de raio de canto. Mapeia para --radius e cascateia em --radius-sm/md/lg/xl — todo elemento arredondado muda junto.
densitycompact | comfortable | spaciousMultiplicador de espaçamento aplicado como a variável CSS --spacing-mul. compact = 0,85×, comfortable = 1,0× (padrão), spacious = 1,2×. Escala o espaçamento base do Tailwind, então padding, gap e utilitários de espaçamento mudam proporcionalmente.
layout:
radius: "0"
density: compact

Variantes de layout​

layoutVariant escolhe o layout geral do shell. Cai para "standard" quando ausente.

VarianteComportamento
standardColuna única, largura máxima de 1600px (padrão).
cockpitTrilho de barra lateral à esquerda (260px) + conteúdo principal. Preenchido por plugins pelo slot sidebar — veja Slots do shell. Sem um plugin, o trilho mostra um espaço reservado.
tiledRemove o limite de largura máxima, para as páginas usarem toda a largura da janela.
layoutVariant: cockpit

A variante atual fica exposta em document.documentElement.dataset.layoutVariant, então CSS cru dentro de customCSS pode mirá-la via :root[data-layout-variant="cockpit"] ....

Ativos de tema (imagens como variáveis CSS)​

Distribua URLs de arte junto com um tema. Cada slot nomeado vira uma variável CSS (--theme-asset-<nome>) que o shell embutido e qualquer plugin podem ler. O slot bg é ligado automaticamente ao pano de fundo; os outros slots são voltados a plugins.

assets:
bg: "https://exemplo.com/hero-bg.jpg" # ligado automaticamente ao <Backdrop />
hero: "/minhas-imagens/strike-freedom.png" # para barras laterais de plugin
crest: "/minhas-imagens/brasao.svg" # para plugins no header-left
logo: "/minhas-imagens/logo.png"
sidebar: "/minhas-imagens/trilho.png"
header: "/minhas-imagens/arte-de-cabecalho.png"
custom:
scanLines: "/minhas-imagens/scanlines.png" # → --theme-asset-custom-scanLines

Os valores aceitam:

  • URLs simples — envolvidas em url(...) automaticamente.
  • Expressões já prontas url(...), linear-gradient(...), radial-gradient(...) — usadas como estão.
  • "none" — recusa explícita.

Todo ativo também é emitido como --theme-asset-<nome>-raw (a URL sem o embrulho), caso um plugin precise passá-la para <img src> em vez de background-image.

Plugins leem isso com CSS ou JS comum:

// Dentro de um slot de plugin
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();

Ajustes de acabamento de componentes​

componentStyles reestiliza componentes individuais do shell sem escrever seletores CSS. As entradas de cada balde viram variáveis CSS (--component-<balde>-<propriedade-em-kebab>) que os componentes compartilhados do shell leem. Assim, card: vale para todo <Card>, header: para a barra do app, e assim por diante.

componentStyles:
card:
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
header:
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
tab:
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
sidebar: {}
backdrop: {}
footer: {}
progress: {}
badge: {}
page: {}

Baldes suportados: card, header, footer, sidebar, tab, progress, badge, backdrop, page.

Os nomes de propriedade usam camelCase (clipPath) e são emitidos em kebab (clip-path). Os valores são strings CSS comuns — qualquer coisa que o CSS aceite (clip-path, border-image, background, box-shadow, animation, ...).

Ajustes de cor​

A maioria dos temas não vai precisar disso — a paleta de 3 camadas deriva todo token do shadcn. Use colorOverrides quando quiser um destaque específico que a derivação não produz (um vermelho destrutivo mais suave para um tema pastel, um verde de sucesso específico de uma marca).

colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"

Chaves suportadas: card, cardForeground, popover, popoverForeground, primary, primaryForeground, secondary, secondaryForeground, muted, mutedForeground, accent, accentForeground, destructive, destructiveForeground, success, warning, border, input, ring.

Cada chave mapeia 1 para 1 na variável CSS --color-<kebab> (por exemplo, primaryForeground → --color-primary-foreground). Qualquer chave definida aqui vence a cascata da paleta, mas só para o tema ativo — trocar de tema limpa as sobrescritas.

customCSS cru​

Para acabamento no nível de seletor que o componentStyles não consegue expressar — pseudoelementos, animações, media queries, sobrescritas com escopo de tema — coloque CSS cru em customCSS:

customCSS: |
/* Sobreposição de scanlines — só visível quando a variante cockpit está ativa. */
:root[data-layout-variant="cockpit"] body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(to bottom,
transparent 0px, transparent 2px,
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
mix-blend-mode: screen;
}

O CSS é injetado como uma única tag <style data-hermes-theme-css> com escopo, na aplicação do tema, e é limpo na troca de tema. Limitado a 32 KiB por tema.

Temas embutidos​

Cada tema embutido traz a própria paleta, tipografia e layout — trocar produz mudanças visíveis além da cor.

TemaPaletaTipografiaLayout
Hermes Teal (default)Verde-azulado escuro + cremePilha do sistema, 15pxRaio 0.5rem, comfortable
Hermes Teal (Large) (default-large)Igual à do padrãoPilha do sistema, 18px, altura de linha 1.65Raio 0.5rem, spacious
Midnight (midnight)Azul-violeta profundoInter + JetBrains Mono, 14pxRaio 0.75rem, comfortable
Ember (ember)Carmesim quente + bronzeSpectral (serifada) + IBM Plex Mono, 15pxRaio 0.25rem, comfortable
Mono (mono)Tons de cinzaIBM Plex Sans + IBM Plex Mono, 13pxRaio 0, compact
Cyberpunk (cyberpunk)Verde neon no pretoShare Tech Mono em tudo, 14pxRaio 0, compact
Rosé (rose)Rosa + marfimFraunces (serifada) + DM Mono, 16pxRaio 1rem, spacious

Temas que usam Google Fonts (todos, menos o Hermes Teal) carregam a folha de estilo sob demanda — na primeira vez que você troca para eles, uma tag <link> é injetada no <head>.

Referência completa do YAML de tema​

Todos os botões em um só arquivo — copie e apare o que você não precisa:

# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Azuis de mar profundo com destaques em coral

# paleta de 3 camadas (aceita {hex, alpha} ou hex simples)
palette:
background:
hex: "#0a1628"
alpha: 1.0
midground:
hex: "#a8d0ff"
alpha: 1.0
foreground:
hex: "#ffffff"
alpha: 0.0
warmGlow: "rgba(255, 107, 107, 0.35)"
noiseOpacity: 0.7

typography:
fontSans: "Poppins, system-ui, sans-serif"
fontMono: "Fira Code, ui-monospace, monospace"
fontDisplay: "Poppins, system-ui, sans-serif" # opcional
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
baseSize: "15px"
lineHeight: "1.6"
letterSpacing: "-0.003em"

layout:
radius: "0.75rem"
density: comfortable

layoutVariant: standard # standard | cockpit | tiled

assets:
bg: "https://exemplo.com/ocean-bg.jpg"
hero: "/minhas-imagens/kraken.png"
crest: "/minhas-imagens/ancora.svg"
logo: "/minhas-imagens/logo.png"
custom:
pattern: "/minhas-imagens/ondas.svg"

componentStyles:
card:
boxShadow: "inset 0 0 0 1px rgba(168, 208, 255, 0.18)"
header:
background: "linear-gradient(180deg, rgba(10, 22, 40, 0.95), rgba(5, 9, 26, 0.9))"

colorOverrides:
destructive: "#ff6b6b"
ring: "#ff6b6b"

customCSS: |
/* Quaisquer ajustes adicionais no nível de seletor */

Recarregue o painel depois de criar o arquivo. Troque de tema ao vivo pela barra de cabeçalho — clique no ícone de paleta. A escolha persiste no config.yaml sob dashboard.theme e é restaurada ao recarregar.


Plugins​

Um plugin de painel é um diretório com um manifest.json, um bundle JS pré-construído e, opcionalmente, um arquivo CSS e um arquivo Python com rotas do FastAPI. Plugins ficam ao lado dos outros plugins do Hermes em ~/.hermes/plugins/<nome>/ — a extensão de painel é uma subpasta dashboard/ dentro desse diretório de plugin, então um mesmo plugin pode estender a CLI/gateway e o painel a partir de uma única instalação.

Plugins não empacotam React nem componentes de UI. Eles usam o SDK de plugins, exposto em window.__HERMES_PLUGIN_SDK__. Isso mantém os bundles minúsculos (tipicamente alguns KB) e evita conflitos de versão.

Início rápido — seu primeiro plugin​

Crie a estrutura de diretórios:

mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist

Escreva o manifesto:

// ~/.hermes/plugins/my-plugin/dashboard/manifest.json
{
"name": "my-plugin",
"label": "My Plugin",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js"
}

Escreva o bundle JS (uma IIFE simples — sem etapa de build):

// ~/.hermes/plugins/my-plugin/dashboard/dist/index.js
(function () {
"use strict";

const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
const { Card, CardHeader, CardTitle, CardContent } = SDK.components;

function MyPage() {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "My Plugin"),
),
React.createElement(CardContent, null,
React.createElement("p", { className: "text-sm text-muted-foreground" },
"Olá da minha aba personalizada no painel.",
),
),
);
}

window.__HERMES_PLUGINS__.register("my-plugin", MyPage);
})();

Recarregue o painel — sua aba aparece na barra de navegação, depois de Skills.

Dispense o React.createElement

Se você prefere JSX, use qualquer bundler (esbuild, Vite, rollup) com o React como externo e saída em IIFE. A única exigência dura é que o arquivo final seja um único arquivo JS carregável via <script>. O React nunca é empacotado; ele vem de SDK.React.

Estrutura de diretórios​

~/.hermes/plugins/my-plugin/
├── plugin.yaml # opcional — manifesto de plugin de CLI/gateway já existente
├── __init__.py # opcional — hooks de CLI/gateway já existentes
└── dashboard/ # extensão de painel
├── manifest.json # obrigatório — configuração da aba, ícone, ponto de entrada
├── dist/
│ ├── index.js # obrigatório — bundle JS pré-construído (IIFE)
│ └── style.css # opcional — CSS próprio
└── plugin_api.py # opcional — rotas de API no backend (FastAPI)

Um único diretório de plugin pode carregar três extensões ortogonais:

  • plugin.yaml + __init__.py — plugin de CLI/gateway (veja a página de plugins).
  • dashboard/manifest.json + dashboard/dist/index.js — plugin de UI do painel.
  • dashboard/plugin_api.py — rotas de backend do painel.

Nenhuma delas é obrigatória; inclua só as camadas de que você precisa.

Referência do manifesto​

{
"name": "my-plugin",
"label": "My Plugin",
"description": "O que este plugin faz",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"override": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
CampoObrigatórioDescrição
nameSimIdentificador único do plugin. Minúsculas, hífens permitidos. Usado em URLs e no registro.
labelSimNome de exibição mostrado na aba de navegação.
descriptionNãoDescrição curta (mostrada nas telas administrativas do painel).
iconNãoNome de ícone do Lucide. Cai para Puzzle. Nomes desconhecidos caem para Puzzle.
versionNãoString semver. Cai para 0.0.0.
tab.pathSimCaminho de URL da aba (por exemplo, /my-plugin).
tab.positionNãoOnde inserir a aba. "end" (padrão), "after:<caminho>" ou "before:<caminho>" — o valor depois dos dois-pontos é o segmento de caminho da aba alvo (sem barra inicial). Exemplos: "after:skills", "before:config".
tab.overrideNãoDefina como um caminho de rota embutida ("/", "/sessions", "/config", ...) para substituir aquela página em vez de adicionar uma aba nova. Veja Substituir páginas embutidas.
tab.hiddenNãoQuando verdadeiro, registra o componente e os slots sem adicionar aba na navegação. Usado por plugins só de slot. Veja Plugins só de slot.
slotsNãoSlots nomeados do shell que este plugin preenche. Só ajuda de documentação — o registro de verdade acontece no bundle JS, via registerSlot(). Listar os slots aqui deixa as telas de descoberta mais informativas.
entrySimCaminho do bundle JS em relação a dashboard/. Cai para dist/index.js.
cssNãoCaminho de um arquivo CSS a ser injetado como tag <link>.
apiNãoCaminho de um arquivo Python com rotas do FastAPI. Montado em /api/plugins/<nome>/.

Ícones disponíveis​

Plugins usam nomes de ícone do Lucide. O painel mapeia por nome — nomes desconhecidos caem silenciosamente para Puzzle.

Mapeados no momento: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap.

Precisa de outro ícone? Abra um PR no ICON_MAP de web/src/App.tsx — é uma mudança puramente aditiva.

O SDK de plugins​

Tudo o que um plugin precisa está em window.__HERMES_PLUGIN_SDK__. Plugins nunca devem importar o React diretamente.

const SDK = window.__HERMES_PLUGIN_SDK__;

// React + hooks
SDK.React // a instância do React
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext

// Componentes de UI (primitivos do shadcn/ui)
SDK.components.Card
SDK.components.CardHeader
SDK.components.CardTitle
SDK.components.CardContent
SDK.components.Badge
SDK.components.Button
SDK.components.Input
SDK.components.Label
SDK.components.Select
SDK.components.SelectOption
SDK.components.Separator
SDK.components.Tabs
SDK.components.TabsList
SDK.components.TabsTrigger
SDK.components.PluginSlot // renderiza um slot nomeado (útil para UIs de plugin aninhadas)

// Cliente da API do Hermes + fetch cru
SDK.api // cliente tipado — getStatus, getSessions, getConfig, ...
SDK.fetchJSON // fetch cru para endpoints próprios (rotas registradas por plugins)

// Utilitários
SDK.utils.cn // combinador de classes do Tailwind (clsx + twMerge)
SDK.utils.timeAgo // "5m ago" a partir de um timestamp unix
SDK.utils.isoTimeAgo // "5m ago" a partir de uma string ISO

// Hooks
SDK.useI18n // hook de i18n para plugins multilíngues

Chamar o backend do seu plugin​

SDK.fetchJSON("/api/plugins/my-plugin/data")
.then((data) => console.log(data))
.catch((err) => console.error("API call failed:", err));

O fetchJSON injeta o token de autenticação da sessão, expõe erros como exceções lançadas e faz o parse do JSON automaticamente.

Chamar endpoints embutidos do Hermes​

// Status do agente
SDK.api.getStatus().then((s) => console.log("Version:", s.version));

// Sessões recentes
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

Veja Painel Web → API REST para a lista completa.

Slots do shell​

Slots deixam um plugin injetar componentes em pontos nomeados do shell do app — a barra lateral do cockpit, o cabeçalho, o rodapé, uma camada de sobreposição — sem tomar uma aba inteira. Vários plugins podem preencher o mesmo slot; eles são renderizados empilhados, na ordem de registro.

Registre de dentro do bundle do plugin:

window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sidebar", MySidebar);
window.__HERMES_PLUGINS__.registerSlot("my-plugin", "header-left", MyCrest);

Catálogo de slots​

Slots de todo o shell (renderizam em qualquer parte do acabamento do app):

SlotLocal
backdropDentro da pilha de camadas do <Backdrop />, acima da camada de ruído.
header-leftAntes da marca Hermes na barra de cima.
header-rightAntes dos seletores de tema/idioma na barra de cima.
header-bannerFaixa de largura total abaixo da navegação.
sidebarTrilho da barra lateral do cockpit — só renderiza quando layoutVariant === "cockpit".
pre-mainAcima do outlet de rota (dentro de <main>).
post-mainAbaixo do outlet de rota (dentro de <main>).
footer-leftConteúdo da célula do rodapé (substitui o padrão).
footer-rightConteúdo da célula do rodapé (substitui o padrão).
overlayCamada de posição fixa acima de todo o resto. Útil para acabamentos (scanlines, vinhetas) que o customCSS não consegue sozinho.

Slots com escopo de página (renderizam só na página embutida indicada — use-os para injetar widgets, cards ou barras de ferramentas em uma página existente sem sobrescrever a rota inteira):

SlotOnde renderiza
sessions:top / sessions:bottomTopo / base da página /sessions.
analytics:top / analytics:bottomTopo / base da página /analytics.
logs:top / logs:bottomTopo (acima da barra de filtros) / base (abaixo do visualizador de logs) de /logs.
cron:top / cron:bottomTopo / base da página /cron.
skills:top / skills:bottomTopo / base da página /skills.
config:top / config:bottomTopo / base da página /config.
env:top / env:bottomTopo / base da página /env (Keys).
docs:top / docs:bottomTopo (acima do iframe) / base de /docs.
chat:top / chat:bottomTopo / base de /chat (só ativo quando o chat embutido está habilitado).

Exemplo — adicionar um card de banner ao topo da página Sessions:

function PinnedSessionsBanner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Nota fixada injetada pelo my-plugin"),
);
}

window.__HERMES_PLUGINS__.registerSlot("my-plugin", "sessions:top", PinnedSessionsBanner);

Combine slots com escopo de página com tab.hidden: true se o seu plugin só complementa páginas existentes e não precisa de aba própria na barra lateral.

O shell só renderiza <PluginSlot name="..." /> para os slots acima. Nomes adicionais são aceitos pelo registro para UIs de plugin aninhadas — um plugin pode expor os próprios slots via SDK.components.PluginSlot.

Re-registro e HMR​

Se o mesmo par (plugin, slot) for registrado duas vezes, a chamada mais recente substitui a anterior — isso combina com o jeito que o HMR do React espera que remontagens de plugin se comportem.

Substituir páginas embutidas (tab.override)​

Definir tab.override como o caminho de uma rota embutida faz o componente do plugin substituir aquela página em vez de adicionar uma aba nova. Útil quando um tema quer uma página inicial personalizada (/), mas quer manter o resto do painel intacto.

{
"name": "my-home",
"label": "Home",
"tab": {
"path": "/my-home",
"override": "/",
"position": "end"
},
"entry": "dist/index.js"
}

Com override definido:

  • O componente original da página em / é removido do roteador.
  • O seu plugin renderiza em / no lugar dele.
  • Nenhuma aba de navegação é adicionada para tab.path (a substituição é justamente o ponto).

Só um plugin pode sobrescrever um dado caminho. Se dois plugins reivindicarem a mesma substituição, o primeiro vence e o segundo é ignorado, com um aviso em modo de desenvolvimento.

Se você só precisa adicionar um card ou barra de ferramentas a uma página existente, sem tomá-la, use slots com escopo de página.

Complementar páginas embutidas (slots com escopo de página)​

A substituição completa via tab.override é pesada — seu plugin passa a ser dono da página inteira, inclusive de qualquer atualização futura que a gente entregue nela. Na maioria das vezes você só quer adicionar um banner, um card ou uma barra de ferramentas a uma página existente. É para isso que servem os slots com escopo de página.

Toda página embutida expõe os slots <pagina>:top e <pagina>:bottom, renderizados no topo e na base da área de conteúdo dela. Seu plugin preenche um deles chamando registerSlot() — a página embutida continua funcionando normalmente, e o seu componente renderiza ao lado.

Slots disponíveis: sessions:*, analytics:*, logs:*, cron:*, skills:*, config:*, env:*, docs:*, chat:* (cada um com :top e :bottom). Veja o catálogo completo em Slots do shell → Catálogo de slots.

Exemplo mínimo — fixar um banner no topo da página Sessions:

// ~/.hermes/plugins/session-notes/dashboard/manifest.json
{
"name": "session-notes",
"label": "Session Notes",
"tab": { "path": "/session-notes", "hidden": true },
"slots": ["sessions:top"],
"entry": "dist/index.js"
}
// ~/.hermes/plugins/session-notes/dashboard/dist/index.js
(function () {
const SDK = window.__HERMES_PLUGIN_SDK__;
const { React } = SDK;
const { Card, CardContent } = SDK.components;

function Banner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Lembre de rotular as sessões importantes antes de arquivar."),
);
}

// Espaço reservado para a aba oculta.
window.__HERMES_PLUGINS__.register("session-notes", function () { return null; });

// O trabalho de verdade.
window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();

Pontos-chave:

  • tab.hidden: true mantém o plugin fora da barra lateral — ele não tem página própria.
  • O campo slots do manifesto é só documentação. A ligação de verdade acontece no bundle JS, via registerSlot().
  • Vários plugins podem reivindicar o mesmo slot com escopo de página. Eles renderizam empilhados, na ordem de registro.
  • Pegada zero quando nenhum plugin se registra: a página embutida renderiza exatamente como antes.

Um plugin de referência (example-dashboard, em hermes-example-plugins) traz uma demo viva que injeta um banner em sessions:top — instale para ver o padrão de ponta a ponta.

Plugins só de slot (tab.hidden)​

Quando tab.hidden: true, o plugin registra o componente dele (para visitas diretas por URL) e os slots, mas nunca adiciona uma aba à navegação. Usado por plugins que só existem para injetar em slots — um brasão no cabeçalho, um HUD na barra lateral, uma sobreposição.

{
"name": "header-crest",
"label": "Header Crest",
"tab": {
"path": "/header-crest",
"position": "end",
"hidden": true
},
"slots": ["header-left"],
"entry": "dist/index.js"
}

O bundle ainda chama register() com um componente de espaço reservado (boa prática, caso alguém acesse a URL diretamente) e depois registerSlot() para fazer o trabalho de verdade.

Rotas de API no backend​

Plugins podem registrar rotas do FastAPI definindo api no manifesto. Crie o arquivo e exporte um router:

# ~/.hermes/plugins/my-plugin/dashboard/plugin_api.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/data")
async def get_data():
return {"items": ["one", "two", "three"]}

@router.post("/action")
async def do_action(body: dict):
return {"ok": True, "received": body}

As rotas são montadas sob /api/plugins/<nome>/, então o exemplo acima vira:

  • GET /api/plugins/my-plugin/data
  • POST /api/plugins/my-plugin/action

As rotas de API de plugin ficam atrás da barreira normal de autenticação do painel — requisições sem autenticação levam 401 antes de a rota do plugin rodar, e requisições às rotas de um plugin desabilitado são rejeitadas na hora. Ainda assim, não exponha o painel em uma interface pública com --host 0.0.0.0 se você roda plugins não confiáveis — uma sessão autenticada também alcança as rotas deles.

Acessar as entranhas do Hermes​

Rotas de backend rodam dentro do processo do painel, então podem importar diretamente do código do hermes-agent:

from fastapi import APIRouter
from hermes_state import SessionDB
from hermes_cli.config import load_config

router = APIRouter()

@router.get("/session-count")
async def session_count():
db = SessionDB()
try:
count = len(db.list_sessions(limit=9999))
return {"count": count}
finally:
db.close()

@router.get("/config-snapshot")
async def config_snapshot():
cfg = load_config()
return {"model": cfg.get("model", {})}

CSS próprio por plugin​

Se o seu plugin precisa de estilos além das classes do Tailwind e do style= inline, adicione um arquivo CSS e aponte para ele no manifesto:

{
"css": "dist/style.css"
}

O arquivo é injetado como uma tag <link> no carregamento do plugin. Use nomes de classe específicos para evitar conflitos com os estilos do painel, e use as variáveis CSS do painel para acompanhar o tema:

/* dist/style.css */
.my-plugin-chart {
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-card-foreground);
padding: 1rem;
}
.my-plugin-chart:hover {
border-color: var(--color-ring);
}

O painel expõe todo token do shadcn como --color-*, mais os extras de tema (--theme-asset-*, --component-<balde>-*, --radius, --spacing-mul). Use essas variáveis e o seu plugin se reestiliza automaticamente com o tema ativo.

Descoberta e recarga de plugins​

O painel varre três diretórios em busca de dashboard/manifest.json:

PrioridadeDiretórioRótulo de origem
1 (vence em conflito)~/.hermes/plugins/<nome>/dashboard/user
2<repo>/plugins/memory/<nome>/dashboard/bundled
2<repo>/plugins/<nome>/dashboard/bundled
3./.hermes/plugins/<nome>/dashboard/project — só quando HERMES_ENABLE_PROJECT_PLUGINS está definida

Os resultados da descoberta ficam em cache por processo do painel. Depois de adicionar um plugin novo, faça uma destas coisas:

# Força uma nova varredura sem reiniciar
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

…ou reinicie o hermes dashboard.

Ciclo de vida do carregamento de plugins​

  1. O painel carrega. O main.tsx expõe o SDK em window.__HERMES_PLUGIN_SDK__ e o registro em window.__HERMES_PLUGINS__.
  2. O App.tsx chama usePlugins() → busca GET /api/dashboard/plugins.
  3. Para cada manifesto: o <link> de CSS é injetado (se declarado), e depois uma tag <script> carrega o bundle JS.
  4. A IIFE do plugin roda e chama window.__HERMES_PLUGINS__.register(name, Component) — e, opcionalmente, .registerSlot(name, slot, Component) para cada slot.
  5. O painel resolve o componente registrado contra o manifesto, adiciona a aba à navegação (a menos que esteja hidden) e monta o componente como uma rota.

Os plugins têm até 2 segundos depois que o script deles carrega para chamar register(). Passado esse tempo, o painel para de esperar e conclui a renderização inicial. Se um plugin se registrar depois, ele ainda aparece — a navegação é reativa.

Se o script de um plugin falhar ao carregar (404, erro de sintaxe, exceção durante a IIFE), o painel registra um aviso no console do navegador e segue sem ele.


Demo combinando tema + plugin​

O plugin strike-freedom-cockpit (repositório companheiro hermes-example-plugins) é uma demo completa de reestilização. Ele junta um YAML de tema com um plugin só de slot para produzir um HUD em estilo de cabine, sem fazer fork do painel.

O que ele demonstra:

  • Um tema completo usando paleta, tipografia, fontUrl, layoutVariant: cockpit, assets, componentStyles (cantos de card chanfrados, fundos em gradiente), colorOverrides e customCSS (sobreposição de scanlines).
  • Um plugin só de slot (tab.hidden: true) que se registra em três slots:
    • sidebar — um painel MS-STATUS com barras de telemetria ao vivo, alimentadas por SDK.api.getStatus().
    • header-left — um brasão de facção que lê --theme-asset-crest do tema ativo.
    • footer-right — um slogan personalizado que substitui a linha padrão da organização.
  • O plugin lê a arte fornecida pelo tema via variáveis CSS, então trocar de tema muda o hero/brasão sem mexer no código do plugin.

Instalação:

git clone https://github.com/NousResearch/hermes-example-plugins.git

# Tema
cp hermes-example-plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
~/.hermes/dashboard-themes/

# Plugin
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/

Abra o painel e escolha Strike Freedom no seletor de temas. A barra lateral de cabine aparece, o brasão surge no cabeçalho, o slogan substitui o rodapé. Volte para Hermes Teal e o plugin continua instalado, mas invisível (o slot sidebar só renderiza sob a variante de layout cockpit).

Leia o código do plugin (strike-freedom-cockpit/dashboard/dist/index.js, no repositório companheiro) para ver como ele lê variáveis CSS, se protege contra painéis antigos sem suporte a slots, e registra três slots a partir de um único bundle.


Referência de API​

Endpoints de tema​

EndpointMétodoDescrição
/api/dashboard/themesGETLista os temas disponíveis + o nome do ativo. Os embutidos retornam {name, label, description}; temas do usuário também trazem um campo definition com o objeto de tema normalizado completo.
/api/dashboard/themePUTDefine o tema ativo. Corpo: {"name": "midnight"}. Persiste no config.yaml sob dashboard.theme.

Endpoints de plugin​

EndpointMétodoDescrição
/api/dashboard/pluginsGETLista os plugins descobertos (com os manifestos, menos os campos internos).
/api/dashboard/plugins/rescanGETForça uma nova varredura dos diretórios de plugin sem reiniciar.
/dashboard-plugins/<nome>/<caminho>GETServe arquivos estáticos do diretório dashboard/ de um plugin. A travessia de caminho é bloqueada.
/api/plugins/<nome>/**Rotas de backend registradas por plugins.

SDK em window​

GlobalTipoProvedor
window.__HERMES_PLUGIN_SDK__objetoregistry.ts — React, hooks, componentes de UI, cliente de API, utilitários.
window.__HERMES_PLUGINS__.register(name, Component)funçãoRegistra o componente principal de um plugin.
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component)funçãoRegistra em um slot nomeado do shell.

Resolução de problemas​

Meu tema não aparece no seletor. Confira se o arquivo está em ~/.hermes/dashboard-themes/ e termina em .yaml ou .yml. Recarregue a página. Rode curl http://127.0.0.1:9119/api/dashboard/themes — o seu tema deve estar na resposta. Se o YAML tem erro de parse, o painel registra em errors.log, dentro de ~/.hermes/logs/.

A aba do meu plugin não aparece.

  1. Confira se o manifesto está em ~/.hermes/plugins/<nome>/dashboard/manifest.json (repare no subdiretório dashboard/).
  2. Rode curl http://127.0.0.1:9119/api/dashboard/plugins/rescan para forçar uma nova descoberta.
  3. Abra as ferramentas de desenvolvedor do navegador → Network — confirme que manifest.json, index.js e qualquer CSS carregaram sem 404.
  4. Abra as ferramentas de desenvolvedor → Console — procure erros durante a IIFE ou window.__HERMES_PLUGINS__ is undefined (indica que o SDK não inicializou, geralmente por uma quebra de renderização do React antes disso).
  5. Verifique se o seu bundle chama window.__HERMES_PLUGINS__.register(...) com o mesmo nome de manifest.json:name.

Componentes registrados em slots não renderizam. O slot sidebar só renderiza quando o tema ativo tem layoutVariant: cockpit. Os outros slots sempre renderizam. Se você está registrando em um slot e não aparece nada, coloque um console.log dentro do registerSlot para confirmar se o bundle do plugin chegou a rodar.

As rotas de backend do plugin retornam 404.

  1. Confirme que o manifesto tem "api": "plugin_api.py" apontando para um arquivo existente dentro de dashboard/.
  2. Reinicie o hermes dashboard — rotas de API de plugin são montadas uma vez, na inicialização, e não na nova varredura.
  3. Confira se o plugin_api.py exporta um router = APIRouter() no nível do módulo. Outros nomes de exportação não são reconhecidos.
  4. Acompanhe o ~/.hermes/logs/errors.log atrás de Failed to load plugin <name> API routes — erros de importação são registrados ali.

A troca de tema derruba meus ajustes de cor. colorOverrides têm escopo do tema ativo e são limpos na troca de tema — isso é de propósito. Se você quer ajustes que persistem, coloque-os no YAML do seu tema, não no seletor ao vivo.

O customCSS do tema é cortado. O bloco customCSS é limitado a 32 KiB por tema. Divida folhas de estilo grandes entre vários temas, ou passe para um plugin que injete uma folha completa pelo campo css dele (sem limite de tamanho).

Quero distribuir um plugin no PyPI. Plugins de painel são instalados por estrutura de diretório, não por entry point do pip. O caminho de distribuição mais limpo hoje é um repositório git que o usuário clona em ~/.hermes/plugins/. Um instalador baseado em pip para plugins de painel não está montado no momento.