Расширение CLI

В HermesCLI есть защищённые хуки расширения: обёртки-CLI добавляют через них виджеты, горячие клавиши и настройки компоновки, не переопределяя метод run() на тысячу с лишним строк. Так расширение остаётся независимым от внутренних изменений.

Точки расширения

Доступно пять швов расширения:

ХукНазначениеКогда переопределять
_get_extra_tui_widgets()Добавить виджеты в компоновкуНужен постоянный UI-элемент (панель, строка статуса, мини-плеер)
_register_extra_tui_keybindings(kb, *, input_area)Добавить горячие клавишиНужны хоткеи (переключение панелей, управление транспортом, горячие клавиши модальных окон)
_build_tui_layout_children(**widgets)Полный контроль над порядком виджетовНужно переставить или обернуть существующие виджеты (редкий случай)
process_command()Добавить кастомные слеш-командыНужна обработка /mycommand (существующий хук)
_build_tui_style_dict()Кастомные стили prompt_toolkitНужны свои цвета или стилизация (существующий хук)

Первые три — новые защищённые хуки. Последние два существовали раньше.

Быстрый старт: обёртка над CLI

#!/usr/bin/env python3
"""my_cli.py — Example wrapper CLI that extends Hermes."""

from cli import HermesCLI
from prompt_toolkit.layout import FormattedTextControl, Window
from prompt_toolkit.filters import Condition


class MyCLI(HermesCLI):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._panel_visible = False

    def _get_extra_tui_widgets(self):
        """Add a toggleable info panel above the status bar."""
        cli_ref = self
        return [
            Window(
                FormattedTextControl(lambda: "📊 My custom panel content"),
                height=1,
                filter=Condition(lambda: cli_ref._panel_visible),
            ),
        ]

    def _register_extra_tui_keybindings(self, kb, *, input_area):
        """F2 toggles the custom panel."""
        cli_ref = self

        @kb.add("f2")
        def _toggle_panel(event):
            cli_ref._panel_visible = not cli_ref._panel_visible

    def process_command(self, cmd: str) -> bool:
        """Add a /panel slash command."""
        if cmd.strip().lower() == "/panel":
            self._panel_visible = not self._panel_visible
            state = "visible" if self._panel_visible else "hidden"
            print(f"Panel is now {state}")
            return True
        return super().process_command(cmd)


if __name__ == "__main__":
    cli = MyCLI()
    cli.run()

Запуск:

cd ~/.hermes/hermes-agent
source .venv/bin/activate
python my_cli.py

Справочник хуков

_get_extra_tui_widgets()

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

def _get_extra_tui_widgets(self) -> list:
    return []  # default: no extra widgets

Каждый виджет должен быть контейнером prompt_toolkit (например, Window, ConditionalContainer, HSplit). Чтобы виджет можно было переключать, оберните его в ConditionalContainer или задайте filter=Condition(...).

from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControl
from prompt_toolkit.filters import Condition

def _get_extra_tui_widgets(self):
    return [
        ConditionalContainer(
            Window(FormattedTextControl("Status: connected"), height=1),
            filter=Condition(lambda: self._show_status),
        ),
    ]

_register_extra_tui_keybindings(kb, *, input_area)

Вызывается после того, как Hermes зарегистрирует собственные горячие клавиши, и до построения компоновки. Свои привязки добавляйте в kb.

def _register_extra_tui_keybindings(self, kb, *, input_area):
    pass  # default: no extra keybindings

Параметры:

  • kb — экземпляр KeyBindings для приложения prompt_toolkit
  • input_area — основной виджет TextArea, если нужно читать или изменять пользовательский ввод
def _register_extra_tui_keybindings(self, kb, *, input_area):
    cli_ref = self

    @kb.add("f3")
    def _clear_input(event):
        input_area.text = ""

    @kb.add("f4")
    def _insert_template(event):
        input_area.text = "/search "

Избегайте конфликтов со встроенными привязками: Enter (отправить), Escape Enter (новая строка), Ctrl-C (прервать), Ctrl-D (выход), Tab (принять автодополнение). Функциональные клавиши F2 и выше, а также комбинации с Ctrl обычно безопасны.

_build_tui_layout_children(**widgets)

Переопределяйте этот хук, только когда нужен полный контроль над порядком виджетов. Большинству расширений хватит _get_extra_tui_widgets().

def _build_tui_layout_children(self, *, sudo_widget, secret_widget,
    approval_widget, clarify_widget, model_picker_widget=None,
    spinner_widget=None, spacer, status_bar, input_rule_top,
    image_bar, input_area, input_rule_bot, voice_status_bar,
    completions_menu) -> list:

Реализация по умолчанию возвращает (виджеты со значением None отфильтровываются):

[
    Window(height=0),       # anchor
    sudo_widget,            # sudo password prompt (conditional)
    secret_widget,          # secret input prompt (conditional)
    approval_widget,        # dangerous command approval (conditional)
    clarify_widget,         # clarify question UI (conditional)
    model_picker_widget,    # model picker overlay (conditional)
    spinner_widget,         # thinking spinner (conditional)
    spacer,                 # fills remaining vertical space
    *self._get_extra_tui_widgets(),  # YOUR WIDGETS GO HERE
    status_bar,             # model/token/context status line
    input_rule_top,         # ─── border above input
    image_bar,              # attached images indicator
    input_area,             # user text input
    input_rule_bot,         # ─── border below input
    voice_status_bar,       # voice mode status (conditional)
    completions_menu,       # autocomplete dropdown
]

Схема компоновки

Стандартная компоновка сверху вниз:

  1. Область вывода — прокручиваемая история диалога
  2. Спейсер
  3. Дополнительные виджеты — из _get_extra_tui_widgets()
  4. Строка статуса — модель, % контекста, прошедшее время
  5. Строка изображений — счётчик прикреплённых изображений
  6. Область ввода — промпт пользователя
  7. Статус голоса — индикатор записи
  8. Меню автодополнения — подсказки автодополнения

Советы

  • Обновляйте отображение после изменений состояния: вызовите self._invalidate(), чтобы prompt_toolkit перерисовал интерфейс.
  • Доступ к состоянию агента: self.agent, self.model, self.conversation_history — всё под рукой.
  • Кастомные стили: переопределите _build_tui_style_dict() и добавьте записи для своих классов стилей.
  • Слеш-команды: переопределите process_command(), обработайте свои команды, а всё остальное передайте в super().process_command(cmd).
  • Не переопределяйте run() без крайней необходимости — хуки расширения для того и сделаны, чтобы такой связки не возникало.
ESC