Расширение Dashboard

Веб-панель Hermes (hermes dashboard) создана для переоформления и расширения без форка репозитория. Доступны три слоя:

  1. Themes — YAML-файлы, которые перекрашивают палитру, типографику, макет и хром компонентов панели. Поместите файл в ~/.hermes/dashboard-themes/ — он появится в переключателе тем.

  2. UI плагины — директория с manifest.json + JavaScript-бандлом, который регистрирует вкладку, заменяет встроенную страницу, дополняет её через слоты области страницы или внедряет компоненты в именованные слоты оболочки.

  3. Backend плагины — Python-файл внутри директории плагина, который предоставляет FastAPI router; маршруты монтируются по адресу /api/plugins/<name>/ и вызываются из UI плагина.

Все три устанавливаются на лету: не требуется клонирование репозитория, npm run build или патчинг исходного кода панели. Эта страница является каноническим справочником для всех трёх.

Если вы просто хотите использовать панель, см. Web Dashboard. Если вы хотите переоформить терминальный CLI (не веб-панель), см. Skins & Themes — система скинов CLI не связана с темами панели.

note Как части сочетаются Темы и плагины независимы, но синергичны. Тема может существовать сама по себе (просто YAML-файл). Плагин может существовать сам по себе (просто вкладка). Вместе они позволяют создать полный визуальный рескин с собственными HUD — демонстрационный бандл strike-freedom-cockpit делает именно это. См. Комбинированная тема + демо плагина.


Содержание


Темы

Темы — это YAML-файлы, хранящиеся в ~/.hermes/dashboard-themes/. Имя файла не имеет значения (система использует поле name: темы), но по соглашению используется <name>.yaml. Каждое поле необязательно — отсутствующие ключи возвращаются к встроенной теме default, поэтому тема может состоять всего из одного цвета.

Быстрый старт — ваша первая тема

mkdir -p ~/.hermes/dashboard-themes
# ~/.hermes/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Pure magenta on black

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

Обновите панель. Нажмите на иконку палитры в заголовке и выберите Neon. Фон станет чёрным, текст и акценты — малиновыми, а все производные цвета (карточка, граница, приглушённый, кольцо и т.д.) будут пересчитаны из этого триплета из 2 цветов через color-mix() в CSS.

Это весь процесс: один файл, два цвета. Всё ниже — необязательные улучшения.

Палитра, типографика, макет

Эти три блока являются основой темы. Каждый независим — переопределите один, оставьте другие.

Палитра (3 слоя)

Палитра — это триплет цветовых слоёв плюс цвет тёплого свечения и множитель шумовой текстуры. Каскад дизайн-системы панели выводит каждый shadcn-совместимый токен (card, popover, muted, border, primary, destructive, ring и т.д.) из этого триплета через CSS color-mix(). Переопределение трёх цветов каскадно меняет весь интерфейс.

Ключ Описание
palette.background Самый глубокий цвет холста — обычно почти чёрный. Определяет фон страницы и заливку карточек.
palette.midground Основной текст и акцент. Большинство элементов интерфейса используют его (основной текст, контуры кнопок, кольца фокуса).
palette.foreground Верхний слой подсветки. Стандартная тема устанавливает его белым с альфой 0 (невидимый); темы, желающие яркий акцент сверху, могут увеличить его альфу.
palette.warmGlow Строка rgba(...), используемая как цвет виньетки компонентом <Backdrop />.
palette.noiseOpacity Множитель 0–1.2 для шумовой текстуры. Меньше = мягче, больше = зернистее.

Каждый слой принимает либо {hex: "#RRGGBB", alpha: 0.0–1.0}, либо простую hex-строку (альфа по умолчанию 1.0).

palette:
  background:
    hex: "#05091a"
    alpha: 1.0
  midground: "#d8f0ff"          # bare hex, alpha = 1.0
  foreground:
    hex: "#ffffff"
    alpha: 0                    # invisible top layer
  warmGlow: "rgba(255, 199, 55, 0.24)"
  noiseOpacity: 0.7

Типографика

Ключ Тип Описание
fontSans string CSS-список семейств шрифтов для основного текста (применяется к html, body).
fontMono string CSS-список семейств шрифтов для блоков кода, <code>, утилит .font-mono.
fontDisplay string Необязательный стек для заголовков/дисплея. Возвращается к fontSans.
fontUrl string Необязательный URL внешней таблицы стилей. Внедряется как <link rel="stylesheet"> в <head> при переключении темы. Один и тот же URL никогда не внедряется дважды. Работает с Google Fonts, Bunny Fonts, самостоятельными @font-face таблицами — всем, на что можно дать ссылку.
baseSize string Корневой размер шрифта — управляет шкалой rem. Например "14px", "16px".
lineHeight string Межстрочный интервал по умолчанию. Например "1.5", "1.65".
letterSpacing string Межбуквенный интервал по умолчанию. Например "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"

Макет

Ключ Значения Описание
radius любая CSS длина ("0", "0.25rem", "0.5rem", "1rem", ...) Токен радиуса углов. Отображается в --radius и каскадно переходит в --radius-sm/md/lg/xl — все скруглённые элементы меняются вместе.
density compact | comfortable | spacious Множитель отступов, применяемый как CSS-переменная --spacing-mul. compact = 0.85×, comfortable = 1.0× (по умолчанию), spacious = 1.2×. Масштабирует базовые отступы Tailwind, поэтому padding, gap и space-between пропорционально сдвигаются.
layout:
  radius: "0"
  density: compact

Варианты макета

layoutVariant выбирает общий макет оболочки. По умолчанию "standard" при отсутствии.

Вариант Поведение
standard Одна колонка, максимальная ширина 1600px (по умолчанию).
cockpit Левая боковая панель (260px) + основной контент. Заполняется плагинами через слот sidebar — см. Слоты оболочки. Без плагина панель показывает заглушку.
tiled Убирает ограничение максимальной ширины, позволяя страницам использовать всю ширину окна.
layoutVariant: cockpit

Текущий вариант доступен как document.documentElement.dataset.layoutVariant, поэтому обычный CSS в customCSS может нацеливаться на него через :root[data-layout-variant="cockpit"] ....

Ресурсы темы (изображения как CSS-переменные)

Добавьте URL изображений в тему. Каждый именованный слот становится CSS-переменной (--theme-asset-<name>), которую могут читать встроенная оболочка и любой плагин. Слот bg автоматически подключается к фону; остальные слоты предназначены для плагинов.

assets:
  bg: "https://example.com/hero-bg.jpg"           # auto-wired into <Backdrop />
  hero: "/my-images/strike-freedom.png"           # for plugin sidebars
  crest: "/my-images/crest.svg"                   # for header-left plugins
  logo: "/my-images/logo.png"
  sidebar: "/my-images/rail.png"
  header: "/my-images/header-art.png"
  custom:
    scanLines: "/my-images/scanlines.png"         # → --theme-asset-custom-scanLines

Значения принимают:

Каждый ресурс также выводится как --theme-asset-<name>-raw (нераспакованный URL), на случай, если плагину нужно передать его в <img src> вместо background-image.

Плагины читают их через обычный CSS или JS:

// In a plugin slot
const hero = getComputedStyle(document.documentElement)
  .getPropertyValue("--theme-asset-hero").trim();

Переопределение хрома компонентов

componentStyles переопределяет стили отдельных компонентов оболочки без написания CSS-селекторов. Записи каждой группы становятся CSS-переменными (--component-<bucket>-<kebab-property>), которые читают общие компоненты оболочки. Таким образом, переопределения card: применяются к каждому <Card>, header: — к панели приложения и т.д.

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: {}

Поддерживаемые группы: card, header, footer, sidebar, tab, progress, badge, backdrop, page.

Имена свойств пишутся в camelCase (clipPath) и выводятся в kebab-case (clip-path). Значения — обычные CSS-строки — всё, что принимает CSS (clip-path, border-image, background, box-shadow, animation, ...).

Переопределение цветов

Большинству тем это не понадобится — 3-слойная палитра выводит каждый shadcn-токен. Используйте colorOverrides, когда нужен определённый акцент, который вывод не даёт (более мягкий красный для разрушительного действия в пастельной теме, определённый зелёный успеха для бренда).

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

Поддерживаемые ключи: card, cardForeground, popover, popoverForeground, primary, primaryForeground, secondary, secondaryForeground, muted, mutedForeground, accent, accentForeground, destructive, destructiveForeground, success, warning, border, input, ring.

Каждый ключ отображается 1:1 в CSS-переменную --color-<kebab> (например, primaryForeground--color-primary-foreground). Любой ключ, установленный здесь, переопределяет каскад палитры только для активной темы — переключение на другую тему очищает переопределения.

Пользовательский CSS (customCSS)

Для стилизации на уровне селекторов, которую componentStyles не может выразить — псевдоэлементы, анимации, медиа-запросы, переопределения в рамках темы — поместите обычный CSS в customCSS:

customCSS: |
  /* Scanline overlay — only visible when cockpit variant is active. */
  :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;
  }

CSS внедряется как один изолированный тег <style data-hermes-theme-css> при применении темы и удаляется при переключении. Ограничение: 32 КБ на тему.

Встроенные темы

Каждая встроенная тема поставляется с собственной палитрой, типографикой и макетом — переключение даёт видимые изменения, выходящие за рамки только цвета.

Тема Палитра Типографика Макет
Hermes Teal (default) Тёмный бирюзовый + кремовый Системный стек, 15px Радиус 0.5rem, comfortable
Hermes Teal (Large) (default-large) Такой же, как default Системный стек, 18px, межстрочный 1.65 Радиус 0.5rem, spacious
Midnight (midnight) Глубокий сине-фиолетовый Inter + JetBrains Mono, 14px Радиус 0.75rem, comfortable
Ember (ember) Тёплый малиновый + бронзовый Spectral (serif) + IBM Plex Mono, 15px Радиус 0.25rem, comfortable
Mono (mono) Оттенки серого IBM Plex Sans + IBM Plex Mono, 13px Радиус 0, compact
Cyberpunk (cyberpunk) Неоново-зелёный на чёрном Share Tech Mono везде, 14px Радиус 0, compact
Rosé (rose) Розовый + слоновая кость Fraunces (serif) + DM Mono, 16px Радиус 1rem, spacious

Темы, ссылающиеся на Google Fonts (все кроме Hermes Teal), загружают таблицу стилей по требованию — при первом переключении на них тег <link> внедряется в <head>.

Полный справочник YAML темы

Все настройки в одном файле — скопируйте и удалите то, что не нужно:

# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents

# 3-layer palette (accepts {hex, alpha} or bare hex)
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"   # optional
  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://example.com/ocean-bg.jpg"
  hero: "/my-images/kraken.png"
  crest: "/my-images/anchor.svg"
  logo: "/my-images/logo.png"
  custom:
    pattern: "/my-images/waves.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: |
  /* Any additional selector-level tweaks */

Обновите панель после создания файла. Переключайте темы в реальном времени из панели заголовка — нажмите на иконку палитры. Выбор сохраняется в config.yaml в разделе dashboard.theme и восстанавливается при перезагрузке.


Плагины

Плагин панели — это директория с manifest.json, предварительно собранным JS-бандлом, опционально CSS-файлом и Python-файлом с FastAPI-маршрутами. Плагины располагаются рядом с другими плагинами Hermes в ~/.hermes/plugins/<name>/ — расширение панели находится в подпапке dashboard/ внутри директории плагина, так что один плагин может расширять и CLI/шлюз, и панель из одной установки.

Плагины не включают React или UI-компоненты. Они используют SDK плагина, доступный через window.__HERMES_PLUGIN_SDK__. Это сохраняет бандлы плагинов крошечными (обычно несколько КБ) и предотвращает конфликты версий.

Быстрый старт — ваш первый плагин

Создайте структуру директорий:

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

Напишите манифест:

// ~/.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"
}

Напишите JS-бандл (обычный IIFE — этап сборки не требуется):

// ~/.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" },
          "Hello from my custom dashboard tab.",
        ),
      ),
    );
  }

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

Обновите панель — ваша вкладка появится в навигационной панели после Skills.

tip Пропустите React.createElement Если вы предпочитаете JSX, используйте любой бандлер (esbuild, Vite, rollup) с React как внешней зависимостью и IIFE-выводом. Единственное требование — конечный файл должен быть одним JS-файлом, загружаемым через <script>. React никогда не встраивается; он берётся из SDK.React.

Структура директорий

~/.hermes/plugins/my-plugin/
├── plugin.yaml              # optional — existing CLI/gateway plugin manifest
├── __init__.py              # optional — existing CLI/gateway hooks
└── dashboard/               # dashboard extension
    ├── manifest.json        # required — tab config, icon, entry point
    ├── dist/
    │   ├── index.js         # required — pre-built JS bundle (IIFE)
    │   └── style.css        # optional — custom CSS
    └── plugin_api.py        # optional — backend API routes (FastAPI)

Одна директория плагина может содержать три ортогональных расширения:

Ни одно из них не обязательно; включайте только те слои, которые вам нужны.

Справочник манифеста

{
  "name": "my-plugin",
  "label": "My Plugin",
  "description": "What this plugin does",
  "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"
}
Поле Обязательное Описание
name Да Уникальный идентификатор плагина. Нижний регистр, дефисы допустимы. Используется в URL и регистрации.
label Да Отображаемое имя, показываемое в навигационной вкладке.
description Нет Краткое описание (показывается в административных поверхностях панели).
icon Нет Имя иконки Lucide. По умолчанию Puzzle. Неизвестные имена возвращаются к Puzzle.
version Нет Строка семантического версионирования. По умолчанию 0.0.0.
tab.path Да URL-путь для вкладки (например, /my-plugin).
tab.position Нет Куда вставить вкладку. "end" (по умолчанию), "after:<path>" или "before:<path>" — значение после двоеточия — это сегмент пути целевой вкладки (без ведущего слеша). Примеры: "after:skills", "before:config".
tab.override Нет Установите встроенный путь маршрута ("/", "/sessions", "/config", ...), чтобы заменить эту страницу вместо добавления новой вкладки. См. Замена встроенных страниц.
tab.hidden Нет Если true, регистрирует компонент и любые слоты без добавления вкладки в навигацию. Используется плагинами только со слотами. См. Плагины только со слотами.
slots Нет Именованные слоты оболочки, которые заполняет этот плагин. Только для документации — фактическая регистрация происходит из JS-бандла через registerSlot(). Перечисление слотов здесь делает поверхности обнаружения более информативными.
entry Да Путь к JS-бандлу относительно dashboard/. По умолчанию dist/index.js.
css Нет Путь к CSS-файлу для внедрения в виде тега <link>.
api Нет Путь к Python-файлу с FastAPI-маршрутами. Монтируется по адресу /api/plugins/<name>/.

Доступные иконки

Плагины используют имена иконок Lucide. Панель отображает их по имени — неизвестные имена незаметно возвращаются к Puzzle.

Текущие сопоставления: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap.

Нужна другая иконка? Откройте PR в ICON_MAP в web/src/App.tsx — чистое аддитивное изменение.

SDK плагина

Всё, что нужно плагину, находится в window.__HERMES_PLUGIN_SDK__. Плагины не должны импортировать React напрямую.

const SDK = window.__HERMES_PLUGIN_SDK__;

// React + hooks
SDK.React                    // the React instance
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext

// UI components (shadcn/ui primitives)
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    // render a named slot (useful for nested plugin UIs)

// Hermes API client + raw fetcher
SDK.api                      // typed client — getStatus, getSessions, getConfig, ...
SDK.fetchJSON                // raw fetch for custom endpoints (plugin-registered routes)

// Utilities
SDK.utils.cn                 // Tailwind class merger (clsx + twMerge)
SDK.utils.timeAgo            // "5m ago" from unix timestamp
SDK.utils.isoTimeAgo         // "5m ago" from ISO string

// Hooks
SDK.useI18n                  // i18n hook for multi-language plugins

Вызов backend вашего плагина

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

fetchJSON внедряет токен аутентификации сессии, отображает ошибки как выбрасываемые исключения и автоматически парсит JSON.

Вызов встроенных endpoints Hermes

// Agent status
SDK.api.getStatus().then((s) => console.log("Version:", s.version));

// Recent sessions
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

Полный список см. в Web Dashboard → REST API.

Слоты оболочки

Слоты позволяют плагину вставлять компоненты в именованные места оболочки приложения — боковую панель, заголовок, подвал, слой наложения — не занимая целую вкладку. Несколько плагинов могут заполнять один и тот же слот; они отображаются стопкой в порядке регистрации.

Регистрация изнутри бандла плагина:

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

Каталог слотов

Слоты уровня оболочки (отображаются в любом месте хрома приложения):

Слот Расположение
backdrop Внутри стека слоёв <Backdrop />, над слоем шума.
header-left Перед брендом Hermes в верхней панели.
header-right Перед переключателями темы/языка в верхней панели.
header-banner Полноширинная полоса под навигацией.
sidebar Панель кабины — отображается только когда layoutVariant === "cockpit".
pre-main Выше выхода маршрута (внутри <main>).
post-main Ниже выхода маршрута (внутри <main>).
footer-left Содержимое ячейки подвала (заменяет умолчание).
footer-right Содержимое ячейки подвала (заменяет умолчание).
overlay Фиксированный слой поверх всего остального. Полезно для хрома (сканлайны, виньетки), который customCSS не может достичь сам по себе.

Слоты области страницы (отображаются только на указанной встроенной странице — используйте их для вставки виджетов, карточек или панелей инструментов на существующую страницу без переопределения всего маршрута):

Слот Где отображается
sessions:top / sessions:bottom Вверху / внизу страницы /sessions.
analytics:top / analytics:bottom Вверху / внизу страницы /analytics.
logs:top / logs:bottom Вверху (над панелью фильтров) / внизу (под просмотрщиком логов) страницы /logs.
cron:top / cron:bottom Вверху / внизу страницы /cron.
skills:top / skills:bottom Вверху / внизу страницы /skills.
config:top / config:bottom Вверху / внизу страницы /config.
env:top / env:bottom Вверху / внизу страницы /env (Keys).
docs:top / docs:bottom Вверху (над iframe) / внизу страницы /docs.
chat:top / chat:bottom Вверху / внизу страницы /chat (активно только когда встроенный чат включён).

Пример — добавьте баннер-карточку в верхнюю часть страницы Sessions:

function PinnedSessionsBanner() {
  return React.createElement(Card, null,
    React.createElement(CardContent, { className: "py-2 text-xs" },
      "Pinned note injected by my-plugin"),
  );
}

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

Комбинируйте слоты области страницы с tab.hidden: true, если ваш плагин только дополняет существующие страницы и не нуждается в собственной вкладке боковой панели.

Оболочка отображает <PluginSlot name="..." /> только для указанных выше слотов. Дополнительные имена принимаются реестром для вложенных UI плагинов — плагин может предоставлять собственные слоты через SDK.components.PluginSlot.

Повторная регистрация и HMR

Если одна и та же пара (plugin, slot) регистрируется дважды, более поздний вызов заменяет более ранний — это соответствует тому, как React HMR ожидает поведения перемонтирования плагинов.

Замена встроенных страниц (tab.override)

Установка tab.override в путь встроенного маршрута заставляет компонент плагина заменять эту страницу вместо добавления новой вкладки. Полезно, когда тема хочет иметь собственную домашнюю страницу (/), но сохранить остальную часть панели нетронутой.

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

При установленном override:

Только один плагин может переопределить данный путь. Если два плагина претендуют на одно и то же переопределение, побеждает первый, а второй игнорируется с предупреждением в режиме разработки.

Если вам нужно только добавить карточку или панель инструментов на существующую страницу без её захвата, используйте слоты области страницы.

Дополнение встроенных страниц (слоты области страницы)

Полная замена через tab.override слишком тяжеловесна — ваш плагин теперь владеет всей страницей, включая любые будущие обновления, которые мы в неё добавим. В большинстве случаев вы просто хотите добавить баннер, карточку или панель инструментов на существующую страницу. Для этого предназначены слоты области страницы.

Каждая встроенная страница предоставляет слоты <page>:top и <page>:bottom, отображаемые вверху и внизу области контента. Ваш плагин заполняет один из них вызовом registerSlot() — встроенная страница продолжает работать normalmente, а ваш компонент отображается рядом с ней.

Доступные слоты: sessions:*, analytics:*, logs:*, cron:*, skills:*, config:*, env:*, docs:*, chat:* (каждый с :top и :bottom). Полный каталог см. в Слоты оболочки → Каталог слотов.

Минимальный пример — закрепить баннер в верхней части страницы 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" },
        "Remember to label important sessions before archiving."),
    );
  }

  // Placeholder for the hidden tab.
  window.__HERMES_PLUGINS__.register("session-notes", function () { return null; });

  // The real work.
  window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();

Ключевые моменты:

Эталонный плагин (example-dashboard в hermes-example-plugins) содержит живое демо, которое вставляет баннер в sessions:top — установите его, чтобы увидеть шаблон от начала до конца.

Плагины только со слотами (tab.hidden)

Когда tab.hidden: true, плагин регистрирует свой компонент (для прямых посещений URL) и любые слоты, но никогда не добавляет вкладку в навигацию. Используется плагинами, которые существуют только для внедрения в слоты — герб заголовка, HUD боковой панели, наложение.

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

Бандл всё равно вызывает register() с компонентом-заглушкой (хорошая практика на случай, если кто-то перейдёт по URL напрямую), а затем registerSlot() для реальной работы.

Backend API маршруты

Плагины могут регистрировать FastAPI-маршруты, установив api в манифесте. Создайте файл и экспортируйте 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}

Маршруты монтируются по адресу /api/plugins/<name>/, поэтому вышеуказанные становятся:

API-маршруты плагинов обходят аутентификацию по токену сессии, так как сервер панели по умолчанию привязывается к localhost. Не открывайте панель на публичном интерфейсе с --host 0.0.0.0, если запускаете ненадёжные плагины — их маршруты также станут доступны.

Доступ к внутренностям Hermes

Backend-маршруты выполняются внутри процесса панели, поэтому они могут импортировать из кодовой базы 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 для плагина

Если вашему плагину нужны стили, выходящие за рамки Tailwind-классов и встроенного style=, добавьте CSS-файл и укажите его в манифесте:

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

Файл внедряется как тег <link> при загрузке плагина. Используйте конкретные имена классов, чтобы избежать конфликтов со стилями панели, и ссылайтесь на CSS-переменные панели, чтобы оставаться в курсе темы:

/* 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);
}

Панель предоставляет каждый shadcn-токен как --color-* плюс дополнительные переменные темы (--theme-asset-*, --component-<bucket>-*, --radius, --spacing-mul). Ссылайтесь на них, и ваш плагин будет автоматически переоформляться при смене активной темы.

Обнаружение и перезагрузка плагинов

Панель сканирует три директории на наличие dashboard/manifest.json:

Приоритет Директория Метка источника
1 (побеждает при конфликте) ~/.hermes/plugins/<name>/dashboard/ user
2 <repo>/plugins/memory/<name>/dashboard/ bundled
2 <repo>/plugins/<name>/dashboard/ bundled
3 ./.hermes/plugins/<name>/dashboard/ project — только когда установлен HERMES_ENABLE_PROJECT_PLUGINS

Результаты обнаружения кэшируются на время процесса панели. После добавления нового плагина либо:

# Force a rescan without restart
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

…либо перезапустите hermes dashboard.

Жизненный цикл загрузки плагина

  1. Панель загружается. main.tsx предоставляет SDK на window.__HERMES_PLUGIN_SDK__ и реестр на window.__HERMES_PLUGINS__.

  2. App.tsx вызывает usePlugins() → загружает GET /api/dashboard/plugins.

  3. Для каждого манифеста: CSS <link> внедряется (если объявлен), затем тег <script> загружает JS-бандл.

  4. IIFE плагина запускается и вызывает window.__HERMES_PLUGINS__.register(name, Component) — и опционально .registerSlot(name, slot, Component) для каждого слота.

  5. Панель сопоставляет зарегистрированный компонент с манифестом, добавляет вкладку в навигацию (если не hidden) и монтирует компонент как маршрут.

У плагинов есть до 2 секунд после загрузки скрипта, чтобы вызвать register(). После этого панель перестаёт ждать и завершает начальный рендеринг. Если плагин регистрируется позже, он всё равно появится — навигация реактивна.

Если скрипт плагина не загружается (404, синтаксическая ошибка, исключение во время IIFE), панель записывает предупреждение в консоль браузера и продолжает работу без него.


Комбинированная тема + демо плагина

Плагин strike-freedom-cockpit (репозиторий-компаньон hermes-example-plugins) — это полная демонстрация переоформления. Он объединяет YAML-тему с плагином только со слотами для создания HUD в стиле кабины без форка панели.

Что демонстрируется:

Установка:

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

# Theme
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/

Откройте панель, выберите Strike Freedom в переключателе тем. Появится боковая панель кабины, герб отобразится в заголовке, строка заменит подвал. Переключитесь обратно на Hermes Teal — плагин останется установленным, но станет невидимым (слот sidebar отображается только при варианте макета cockpit).

Прочитайте исходный код плагина (strike-freedom-cockpit/dashboard/dist/index.js в репозитории-компаньоне), чтобы увидеть, как он читает CSS-переменные, защищается от старых панелей без поддержки слотов и регистрирует три слота из одного бандла.


Справочник API

Endpoints тем

Endpoint Метод Описание
/api/dashboard/themes GET Список доступных тем + имя активной. Встроенные возвращают {name, label, description}; пользовательские темы также включают поле definition с полным нормализованным объектом темы.
/api/dashboard/theme PUT Установить активную тему. Тело: {"name": "midnight"}. Сохраняется в config.yaml в разделе dashboard.theme.

Endpoints плагинов

Endpoint Метод Описание
/api/dashboard/plugins GET Список обнаруженных плагинов (с манифестами, без внутренних полей).
/api/dashboard/plugins/rescan GET Принудительное повторное сканирование директорий плагинов без перезапуска.
/dashboard-plugins/<name>/<path> GET Обслуживание статических ресурсов из директории dashboard/ плагина. Path traversal заблокирован.
/api/plugins/<name>/* * Backend-маршруты, зарегистрированные плагином.

SDK на window

Глобальная переменная Тип Поставщик
window.__HERMES_PLUGIN_SDK__ object registry.ts — React, хуки, UI-компоненты, API-клиент, утилиты.
window.__HERMES_PLUGINS__.register(name, Component) function Регистрация основного компонента плагина.
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component) function Регистрация в именованном слоте оболочки.

Устранение неполадок

Моя тема не отображается в переключателе. Проверьте, что файл находится в ~/.hermes/dashboard-themes/ и заканчивается на .yaml или .yml. Обновите страницу. Выполните curl http://127.0.0.1:9119/api/dashboard/themes — ваша тема должна быть в ответе. Если в YAML есть ошибка парсинга, панель записывает её в errors.log в ~/.hermes/logs/.

Вкладка моего плагина не отображается.

  1. Проверьте, что манифест находится по адресу ~/.hermes/plugins/<name>/dashboard/manifest.json (обратите внимание на поддиректорию dashboard/).

  2. Выполните curl http://127.0.0.1:9119/api/dashboard/plugins/rescan для принудительного повторного обнаружения.

  3. Откройте инструменты разработчика браузера → Network — убедитесь, что manifest.json, index.js и CSS загрузились без 404.

  4. Откройте инструменты разработчика браузера → Console — ищите ошибки во время IIFE или window.__HERMES_PLUGINS__ is undefined (указывает на то, что SDK не инициализировался, обычно из-за сбоя рендеринга React).

  5. Убедитесь, что ваш бандл вызывает window.__HERMES_PLUGINS__.register(...) с тем же именем, что и manifest.json:name.

Компоненты, зарегистрированные в слотах, не отображаются. Слот sidebar отображается только когда активная тема имеет layoutVariant: cockpit. Другие слоты отображаются всегда. Если вы регистрируетесь в слот без попаданий, добавьте console.log внутри registerSlot, чтобы подтвердить, что бандл плагина вообще запустился.

Backend-маршруты плагина возвращают 404.

  1. Убедитесь, что в манифесте есть "api": "plugin_api.py", указывающий на существующий файл внутри dashboard/.

  2. Перезапустите hermes dashboard — API-маршруты плагина монтируются один раз при запуске, а не при повторном сканировании.

  3. Проверьте, что plugin_api.py экспортирует модульный router = APIRouter(). Другие имена экспорта не подхватываются.

  4. Просмотрите ~/.hermes/logs/errors.log в поисках Failed to load plugin <name> API routes — ошибки импорта записываются туда.

Смена темы сбрасывает мои переопределения цветов. colorOverrides ограничены рамками активной темы и очищаются при переключении темы — это сделано намеренно. Если вам нужны переопределения, которые сохраняются, поместите их в YAML вашей темы, а не в живой переключатель.

Пользовательский CSS темы обрезается. Блок customCSS ограничен 32 КБ на тему. Разделите большие таблицы стилей на несколько тем или переключитесь на плагин, который внедряет полную таблицу стилей через поле css (без ограничения размера).

Я хочу опубликовать плагин на PyPI. Плагины панели устанавливаются по структуре директорий, а не через точку входа pip. Самый чистый путь распространения на сегодня — git-репозиторий, который пользователь клонирует в ~/.hermes/plugins/. Установщик на основе pip для плагинов панели в настоящее время не настроен.