Расширение панели управления

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

  1. Темы — YAML-файлы, которые перекрашивают палитру, типографику, макет и хромированные отдельные компоненты панели. Поместите файл в ~/.hermes/dashboard-themes/; он появляется в переключателе тем.
  2. UI-плагины — каталог с manifest.json + JavaScript-бандл, который регистрирует вкладку, заменяет встроенную страницу, выполняет ее через слоты на странице или внедряет компоненты в именованные слоты обработки.
  3. Серверные плагины — Python-файл внутри той же директории плагина, который предоставляет router FastAPI; Маршруты монтируются по пути /api/plugins/<name>/ и вызываются из плагина пользовательского интерфейса.

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

Если вы просто хотите использовать панель, см. Веб-панель. Если вы хотите изменить облик терминального CLI (не веб-панели), см. Скины и темы — система скинов CLI, не связанная с темами панели.

📝 Note

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


Содержание


Темы

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

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

mkdir -p ~/.hermes/dashboard-themes
# ~/.hermes/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Чистый маджента на чёрном

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

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

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

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

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

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

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

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

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

palette:
  background:
    hex: "#05091a"
    alpha: 1.0
  midground: "#d8f0ff"          # голая hex, alpha = 1.0
  foreground:
    hex: "#ffffff"
    alpha: 0                    # невидимый верхний слой
  warmGlow: "rgba(255, 199, 55, 0.24)"
  noiseOpacity: 0.7

Типографика

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

Макет

Ключ Значения Описание
радиус любая CSS-длина("0", "0.25rem", "0.5rem", "1rem",...) Токен радиуса угла. Отображается на --radius и каскадируется на --radius-sm/md/lg/xl — все скруглённые элементы изменяются вместе.
плотность компактный | комфортно | просторный Множитель интервалов, примененный как переменная CSS --spacing-mul. компактный = 0,85×, комфортный = 1,0× (по умолчанию), просторный = 1,2×. Масштабирует базовые интервалы попутного ветра, так же как и отступы, промежутки и межблочные расстояния, преобразующие перемену.
layout:
  radius: "0"
  density: compact

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

layoutVariant выбирает общий макет изготовления. По умолчанию `"стандарт", если отсутствует.

Вариант Поведение
стандарт Одна колонка, максимальная ширина 1600 пикселей (по умолчанию).
кабина Левая боковая панель (260 пикселей) + основной контент. Заполняется плагин через слот sidebar — см. Слоты обработки. Без вилки панели откроется заглушка.
плиточный Убирает ограничение по краю, чтобы можно было использовать весь край окна для просмотра.
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"           # автоматически подключается к <Backdrop />
  hero: "/my-images/strike-freedom.png"           # для боковых панелей плагинов
  crest: "/my-images/crest.svg"                   # для плагинов слева в заголовке
  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:

// В слоте плагина
const hero = getComputedStyle(document.documentElement).getPropertyValue("--theme-asset-hero").trim();

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

comComponentStyles переопределяет стили отдельных компонентов без написания CSS-селекторов. Записи каждого бакета становятся CSS-переменными (--comment-<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(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, вторичный, SecondaryForeground, muted, mutedForeground, accent, accentForeground, destructive, destructiveForeground, success, warning, border, вход, кольцо.

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

Сырой customCSS

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

customCSS: |
  /* Наложение строк развёртки — видно только при активном варианте cockpit. */: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 КБ на теме.

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

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

Тема Палитра Типографика Макет
Гермес Тил (по умолчанию) Тёмный бирюзовый + кремовый Системный стек, 15px Радиус 0,5 бэр, удобный
Гермес Тил (Большой) (default-large) То же самое, что по умолчанию Системный стек, 18px, line-height 1.65 Радиус 0,5 бэр, просторный
Полночь («полночь») Глубокий сине-фиолетовый Интер + JetBrains Mono, 14 пикселей Радиус 0,75 бэр, удобный
Эмбер (уголь) Тёплый малиновый + бронзовый Spectral (с засечками) + IBM Plex Mono, 15px Радиус 0,25 бэр, удобный
Моно («моно») Оттенки серого IBM Plex Sans + IBM Plex Mono, 13 пикселей 0 радиус, компактный
Киберпанк («киберпанк») Неоново-зелёный на чёрном Поделиться Tech Mono везде, 14 пикселей 0 радиус, компактный
Rosé («роза») Розовый + слоновая кость Fraunces (с засечками) + DM Mono, 16px радиус 1 рем, просторный

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

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

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

# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Глубокие морские синие с коралловыми акцентами

# 3-слойная палитра (принимает {hex, alpha} или голую 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"   # опционально
  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: |
  /* Любые дополнительные настройки на уровне селекторов */

Обновите панель после создания файла. Переключите тему в первый раз из заголовка панели — нажмите на иконку палитры. Выбор сохраняется в 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": "Мой плагин",
  "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, "Мой плагин"),
      ),
      React.createElement(CardContent, null,
        React.createElement("p", { className: "text-sm text-muted-foreground" },
          "Привет от моей пользовательской вкладки панели.",
        ),
      ),
    );
  }

  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              # опционально — существующий манифест плагина CLI/шлюза
├── __init__.py              # опционально — существующие хуки CLI/шлюза
└── dashboard/               # расширение панели
    ├── manifest.json        # обязательно — конфигурация вкладки, иконка, точка входа
    ├── dist/
    │   ├── index.js         # обязательно — предварительно собранный JS-бандл (IIFE)
    │   └── style.css        # опционально — пользовательский CSS
    └── plugin_api.py        # опционально — серверные API-маршруты (FastAPI)

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

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

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

{
  "name": "my-plugin",
  "label": "Мой плагин",
  "description": "Что делает этот плагин",
  "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"
}
Поле Обязательно Описание
имя Да Уникальный идентификатор плагина. Нижний регистр, дефисы допустимы. Используется в URL и регистрации.
этикетка Да Отображаемое имя, показываемое на вкладке навигации.
описание Нет Краткое описание (показывается на выходе верхних сторон панели).
значок Нет Имя иконки Люсиде. По умолчанию Пазл. Неизвестные имена возвращаются к Puzzle.
версия Нет Строка Семвер. По умолчанию 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, регистрирует компоненты и любые слоты без добавления вкладки в навигацию. Использовались плагины только со слотами. См. Плагины только со слотами.
слоты Нет Именованные слоты обработки, которые выполняют этот плагин. Только для документации — фактическая регистрация происходит из JS-бандла через registerSlot(). Перечисление слотов здесь делает поиск поверхности более информативным.
вход Да Путь к JS-бандлу относительно dashboard/. По умолчанию dist/index.js.
css Нет Путь к CSS-файлу для развития в видеотеге <link>.
апи Нет Путь к Python-файлу с маршрутами FastAPI. Монтируется по пути /api/plugins/<имя>/.

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

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

В настоящее время структуры: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Гаечный ключ, Зап.

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

Плагин SDK

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

const SDK = window.__HERMES_PLUGIN_SDK__;

// React + хуки
SDK.React                    // экземпляр React
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext

// UI-компоненты (примитивы 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    // отобразить именованный слот (полезно для вложенных UI плагинов)

// Клиент API Hermes + сырой загрузчик
SDK.api                      // типизированный клиент — getStatus, getSessions, getConfig,...
SDK.fetchJSON                // сырой fetch для пользовательских конечных точек (маршруты, зарегистрированные плагином)

// Утилиты
SDK.utils.cn                 // объединитель классов Tailwind (clsx + twMerge)
SDK.utils.timeAgo            // "5m ago" из unix-метки времени
SDK.utils.isoTimeAgo         // "5m ago" из ISO-строки

// Хуки
SDK.useI18n                  // хук i18n для многоязычных плагинов

Вызов серверной части вашего плагина

SDK.fetchJSON("/api/plugins/my-plugin/data").then((data) => console.log(data)).catch((err) => console.error("API-вызов не удался:", err));

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

Вызов внутренних конечных точек Hermes

// Статус агента
SDK.api.getStatus().then((s) => console.log("Версия:", s.version));

// Недавние сессии
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));

См. Веб-панель → REST API для всего списка.

Слоты обработки

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

Зарегистрируйтесь в связном разъеме:

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

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

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

Слот Расположение
фон Внутри стека слоёв <Backdrop />, над слоем шума.
заголовок слева Перед брендом Hermes на верхней панели.
заголовок справа Перед переключателями темы/языка в верхней панели.
шапка-баннер Полноширинная полоса под навигацией.
боковая панель Боковая панель кабины — отображается только при layoutVariant === "cockpit".
предварительный Над выходным маршрутом (внутри <main>).
пост-основной Под выходом маршрута (внутри <main>).
нижний колонтитул слева Содержимое камеры нижней колонтитулы (заменяет значение по умолчанию).
нижний колонтитул справа Содержимое камеры нижней колонтитулы (заменяет значение по умолчанию).
наложение Слой с фиксированной позицией над всеми контактами. Полезно для хрома (строки развёртки, виньетки), который customCSS не может ограничиться одной.

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

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

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

function PinnedSessionsBanner() {
  return React.createElement(Card, null,
    React.createElement(CardContent, { className: "py-2 text-xs" },
      "Закреплённая заметка, внедрённая my-plugin"),
  );
}

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

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

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

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

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

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

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

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

С установленным override:

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

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

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

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

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

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

Минимальный пример — закрепить баннер в верхней части страницы. Сессии:

// ~/.hermes/plugins/session-notes/dashboard/manifest.json
{
  "name": "session-notes",
  "label": "Заметки сессий",
  "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" },
        "Не забудьте пометить важные сессии перед архивированием."),
    );
  }

  // Заглушка для скрытой вкладки.
  window.__HERMES_PLUGINS__.register("session-notes", function () { return null; });

  // Настоящая работа.
  window.__HERMES_PLUGINS__.registerSlot("session-notes", "sessions:top", Banner);
})();

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

tab.hidden: true собирает плагин из боковой панели — у него нет отдельной страницы. - Поле «слотов» в манифесте предназначено только для документации. Фактическое соединение происходит в JS-бандле через registerSlot(). - Несколько плагинов могут претендовать на один и тот же слот на странице. Они предоставлены в порядке регистрации. - Нулевой след, когда ни один плагин не зарегистрирован: встроенная страница отображается точно так же, как и раньше.

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

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

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

{
  "name": "header-crest",
  "label": "Герб заголовка",
  "tab": {
    "path": "/header-crest",
    "position": "end",
    "hidden": true
  },
  "slots": ["header-left"],
  "entry": "dist/index.js"
}

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

Серверные 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

Серверные маршруты выполняются внутри панели процесса, поэтому они могут импортировать из кодовой базы 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-*, --comComponent-<bucket>-*, --radius, --spacing-mul). Ссылайтесь на них, и ваш штекер автоматически перекрашивается в соответствии с активной темой.

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

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

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

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

# Принудительное повторное сканирование без перезапуска
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

…либо перезапустите приборную панель Гермеса.

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

  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. Панель содержит зарегистрированный компонент в манифесте, включает вкладку для навигации (если она не «скрыта») и монтирует компонент по маршруту.

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

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


Демо обнаружения темы и плагина

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

Что он приезжает:

Установка:

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

# Тема
cp hermes-example-plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
   ~/.hermes/dashboard-themes/

# Плагин
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

Конечные точки тем

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

Конечные точки плагинов

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

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. Откройте инструменты разработчика браузера → Сеть — убедитесь, что manifest.json, index.js и любой CSS загрузились без 404. 4. Откройте инструменты разработчика браузера → Консоль — ищите ошибки во время IIFE или window.__HERMES_PLUGINS__ is undefined (указывает на то, что SDK не инициализировался, обычно из-за сбоя рендеринга React ранее). 5. Убедитесь, что ваш бандл вызывает window.__HERMES_PLUGINS__.register(...) с тем же именем, что и manifest.json:name.

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

Серверные маршруты плагина возвращают 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 вашей темы, а не в живой переключатель.

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

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