跳到主要内容

扩展 CLI

Hermes 在 HermesCLI 上暴露了受保护的扩展钩子(extension hooks),使 Wrapper CLI 可以在不覆盖超过 1000 行的 run() 方法的情况下添加组件、快捷键和布局自定义。这让你的扩展与内部变更解耦。

扩展点

共有五个扩展接缝(extension seams)可用:

钩子用途何时覆盖
_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 样式需要自定义颜色或样式时(已有钩子)

前三个是新增的受保护钩子,后两个已经存在。

快速上手:Wrapper CLI

#!/usr/bin/env python3
"""my_cli.py — 扩展 Hermes 的示例 Wrapper CLI。"""

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):
"""在状态栏上方添加一个可切换的信息面板。"""
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 切换自定义面板。"""
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 斜杠命令。"""
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()

返回要插入 TUI 布局的 prompt_toolkit 组件列表。组件出现在spacer 和状态栏之间 — 位于输入区域上方、主输出下方。

def _get_extra_tui_widgets(self) -> list:
return [] # 默认:无额外组件

每个组件应为 prompt_toolkit 容器(如 WindowConditionalContainerHSplit)。使用 ConditionalContainerfilter=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 # 默认:无额外快捷键

参数:

  • kb — prompt_toolkit 应用程序的 KeyBindings 实例
  • 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), # 锚点
sudo_widget, # sudo 密码提示(条件显示)
secret_widget, # 密钥输入提示(条件显示)
approval_widget, # 危险命令审批(条件显示)
clarify_widget, # 澄清问题 UI(条件显示)
model_picker_widget, # 模型选择器覆盖层(条件显示)
spinner_widget, # 思考中 spinner(条件显示)
spacer, # 填充剩余垂直空间
*self._get_extra_tui_widgets(), # 你的组件在此处
status_bar, # 模型/Token/上下文状态行
input_rule_top, # ─── 输入框上方边框
image_bar, # 已附加图片指示器
input_area, # 用户文字输入
input_rule_bot, # ─── 输入框下方边框
voice_status_bar, # 语音模式状态(条件显示)
completions_menu, # 自动补全下拉菜单
]

布局示意图

从上到下的默认布局:

  1. 输出区域 — 滚动的对话历史
  2. Spacer
  3. 额外组件 — 来自 _get_extra_tui_widgets()
  4. 状态栏 — 模型、上下文占比、已用时间
  5. 图片栏 — 已附加图片数量
  6. 输入区域 — 用户提示词
  7. 语音状态 — 录音指示器
  8. 补全菜单 — 自动补全建议

技巧

  • 状态变化后刷新显示:调用 self._invalidate() 触发 prompt_toolkit 重绘。
  • 访问 Agent 状态self.agentself.modelself.conversation_history 均可访问。
  • 自定义样式:覆盖 _build_tui_style_dict() 并为自定义样式类添加条目。
  • 斜杠命令:覆盖 process_command(),处理你的命令后对其他命令调用 super().process_command(cmd)
  • 除非绝对必要,不要覆盖 run() — 扩展钩子的存在正是为了避免这种耦合。