Hermes 在 HermesCLI 上暴露了受保护的扩展 hooks,因此 wrapper CLIs 可以添加 widgets、keybindings 和 layout customizations,而无需 override 超过 1000 行的 run() 方法。这可以让你的扩展与内部变更保持解耦。
目前有五个可用的扩展接缝:
| Hook | Purpose | Override when… |
|---|---|---|
_get_extra_tui_widgets() | 将 widgets 注入 layout | 你需要一个持久 UI 元素(panel、status line、mini-player) |
_register_extra_tui_keybindings(kb, *, input_area) | 添加 keyboard shortcuts | 你需要 hotkeys(toggle panels、transport controls、modal shortcuts) |
_build_tui_layout_children(**widgets) | 完全控制 widget ordering | 你需要重新排序或包裹现有 widgets(少见) |
process_command() | 添加自定义 slash commands | 你需要 /mycommand handling(已有 hook) |
_build_tui_style_dict() | 自定义 prompt_toolkit styles | 你需要自定义 colors 或 styling(已有 hook) |
前三个是新的 protected hooks。后两个已经存在。
快速开始:一个 wrapper CLI
Section titled “快速开始:一个 wrapper CLI”#!/usr/bin/env python3"""my_cli.py — 扩展 Hermes 的示例 wrapper CLI。"""
from cli import HermesCLIfrom prompt_toolkit.layout import FormattedTextControl, Windowfrom 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): """在 status bar 上方添加一个可切换的信息 panel。""" 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 切换 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: """添加一个 /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-agentsource .venv/bin/activatepython my_cli.pyHook 参考
Section titled “Hook 参考”_get_extra_tui_widgets()
Section titled “_get_extra_tui_widgets()”返回要插入 TUI layout 的 prompt_toolkit widgets 列表。Widgets 会出现在 spacer 和 status bar 之间 —— 位于 input area 上方,但在 main output 下方。
def _get_extra_tui_widgets(self) -> list: return [] # 默认:没有额外 widgets每个 widget 都应该是一个 prompt_toolkit container(例如 Window、ConditionalContainer、HSplit)。使用 ConditionalContainer 或 filter=Condition(...) 让 widgets 可切换。
from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControlfrom 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)
Section titled “_register_extra_tui_keybindings(kb, *, input_area)”在 Hermes 注册自己的 keybindings 之后、layout 构建之前调用。将你的 keybindings 添加到 kb。
def _register_extra_tui_keybindings(self, kb, *, input_area): pass # 默认:没有额外 keybindings参数:
kb——prompt_toolkitapplication 的KeyBindings实例input_area—— 主TextAreawidget,如果你需要读取或操作用户输入
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 "避免与内置 keybindings 冲突:Enter(submit)、Escape Enter(newline)、Ctrl-C(interrupt)、Ctrl-D(exit)、Tab(auto-suggest accept)。Function keys F2+ 和 Ctrl-combinations 通常是安全的。
_build_tui_layout_children(**widgets)
Section titled “_build_tui_layout_children(**widgets)”只有当你需要完全控制 widget ordering 时才 override 它。大多数扩展应该改用 _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 widgets 都会被过滤掉):
[ 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, # 填充剩余 vertical space *self._get_extra_tui_widgets(), # 你的 WIDGETS 放在这里 status_bar, # model/token/context status line input_rule_top, # ─── input 上方 border image_bar, # attached images indicator input_area, # user text input input_rule_bot, # ─── input 下方 border voice_status_bar, # voice mode status(conditional) completions_menu, # autocomplete dropdown]Layout diagram
Section titled “Layout diagram”默认 layout 从上到下:
- Output area —— 滚动 conversation history
- Spacer
- Extra widgets —— 来自
_get_extra_tui_widgets() - Status bar —— model、context %、elapsed time
- Image bar —— attached image count
- Input area —— user prompt
- Voice status —— recording indicator
- Completions menu —— autocomplete suggestions
- 状态变化后刷新显示:调用
self._invalidate()触发prompt_toolkitredraw。 - 访问 agent state:
self.agent、self.model、self.conversation_history都可用。 - Custom styles:Override
_build_tui_style_dict(),并为你的 custom style classes 添加 entries。 - Slash commands:Override
process_command(),处理你的 commands,并对其他所有内容调用super().process_command(cmd)。 - 除非绝对必要,否则不要 override
run()—— 这些 extension hooks 的存在正是为了避免这种耦合。