.. _chapter-cli-ui:
***************************************
CLI 与 UI 系统:构建交互式 Agent 界面
***************************************
.. contents::
:local:
:depth: 2
为什么 Agent 需要精心设计的 UI
================================
在传统的认知中,命令行工具只是一种朴素的人机接口——黑底白字,输入命令,得到输出。
然而,当我们将大语言模型(LLM)引入终端后,CLI 的角色发生了质变:
它不再只是"执行命令的工具",而是一个 **人机协作的实时交互界面** 。
Hermes Agent 的 CLI 面临的设计挑战远超一般终端应用:
- **长时间运行的异步任务** :Agent 的一次请求可能触发多次工具调用(终端命令、文件读写、网页搜索),
总耗时从数秒到数分钟不等。用户需要一个实时反馈机制来了解 Agent 正在做什么。
- **多模态交互** :用户可能需要输入密码(sudo)、选择方案(clarify)、审批危险命令(approval),
这些交互模式需要安全的输入通道,且不能干扰正在运行的任务。
- **丰富的视觉反馈** :工具执行状态、文件编辑 diff、token 使用量、模型推理过程——
这些信息的展示方式直接影响用户的工作效率。
- **主题个性化** :不同用户有不同的审美偏好,有人喜欢暗色主题,有人需要亮色适配,
而有些用户希望 Agent 拥有独特的人格化视觉风格。
Hermes 的答案是:用 **prompt_toolkit** 构建一个全功能的 TUI(Terminal User Interface),
同时为非交互式场景(管道、Docker、systemd)提供优雅降级。
皮肤系统让用户可以一键切换视觉风格,Kawaii Spinner 为等待过程注入趣味,
内联 diff 让代码审查变得直观——这些细节共同构成了一套 **令人愉悦的 Agent 交互体验** 。
本章将从架构总览开始,深入剖析 Hermes CLI 的每一个 UI 组件。
CLI 架构总览
==============
Hermes CLI 的启动流程遵循一个清晰的分层架构。从用户在终端输入 ``hermes`` 开始,
到 REPL 循环就绪,经历了以下阶段:
.. mermaid::
:name: cli-architecture-overview
:caption: CLI 架构总览
flowchart TD
A["用户执行 hermes"] --> B["hermes_cli/main.py
argparse 子命令解析"]
B --> C{"子命令类型?"}
C -->|chat / 默认| D["HermesCLI() 初始化"]
C -->|gateway| E["tui_gateway/entry.py
JSON-RPC 网关"]
C -->|setup| F["交互式配置向导"]
C -->|其他子命令| G["一次性执行并退出"]
D --> H["load_cli_config()
加载 config.yaml"]
H --> I["init_skin_from_config()
初始化皮肤引擎"]
I --> J["HermesCLI.run()
启动 REPL 循环"]
J --> K["prompt_toolkit Application"]
K --> L["Layout 构建
输入区 + Spinner + 状态栏"]
K --> M["KeyBindings 注册
Enter / Ctrl+C / Tab"]
K --> N["Completer 注册
Slash 命令补全"]
K --> O["AutoSuggest 注册
历史建议"]
L --> P["REPL 主循环
等待输入 → 路由 → 执行"]
P --> Q{"输入类型?"}
Q -->|Slash 命令| R["命令处理器"]
Q -->|普通文本| S["AIAgent.run_conversation()"]
Q -->|文件拖放| T["附件处理"]
S --> U["流式输出 / Spinner / Diff"]
U --> P
入口点解析
------------
``hermes_cli/main.py`` 是整个系统的入口。它使用 Python Fire 库将 ``HermesCLI`` 类
暴露为 CLI 命令,但在此之前完成了一系列关键的初始化工作:
**Profile 覆盖机制** :在所有模块导入之前,从 ``sys.argv`` 中解析 ``--profile`` / ``-p`` 参数,
设置 ``HERMES_HOME`` 环境变量。这是因为许多模块在导入时就会缓存 ``HERMES_HOME`` 的值(模块级常量),
如果不在最早时机设置,后续的配置路径就会错误。
**.env 加载** :按照优先级顺序加载环境变量——先 ``~/.hermes/.env`` ,再项目根目录的 ``.env`` 。
用户管理的 .env 文件应该覆盖过时的 shell 导出值。
**配置桥接** :``load_cli_config()`` 从 ``config.yaml`` 读取配置,将终端配置映射为环境变量(如
``terminal.env_type`` → ``TERMINAL_ENV``),这样底层的 ``terminal_tool`` 可以通过
``os.getenv()`` 获取配置值。
配置加载的优先级链条为:CLI 参数 > 环境变量 > config.yaml > 默认值。
HermesCLI 类的生命周期
------------------------
``HermesCLI`` 是整个交互式 REPL 的核心。它的初始化参数包括:
- ``model`` :使用的模型名称(如 ``anthropic/claude-sonnet-4``)
- ``toolsets`` :启用的工具集列表(如 ``["web", "terminal", "file"]``)
- ``provider`` :推理服务提供者(如 ``auto`` 、``openrouter`` 、``openai``)
- ``compact`` :紧凑显示模式
- ``resume`` :恢复之前的会话 ID
- ``checkpoints`` :启用文件系统检查点
初始化时会设置大量状态变量,包括对话历史、输入队列、中断队列、各种交互状态
(clarify、sudo、approval、secret、model picker)、语音模式状态等。
``AIAgent`` 实例本身被延迟创建——直到用户发送第一条消息时才真正初始化,避免启动时的长时间等待。
prompt_toolkit 集成
=====================
Hermes 使用 ``prompt_toolkit`` 库来构建一个功能完备的 TUI。
这是一个被 IPython、ptpython 等项目广泛使用的高质量终端 UI 框架。
Layout 布局结构
-----------------
TUI 的布局由 ``HSplit`` 垂直排列的多个区域组成:
.. code-block:: python
# 简化的布局结构
layout = HSplit([
Window(FormattedTextControl(input_area), height=Dimension(min=1)), # 输入区
Window(FormattedTextControl(spinner_widget), height=spinner_height), # Spinner
ConditionalContainer( # 状态栏
Window(FormattedTextControl(status_bar), height=1),
filter=Condition(lambda: self._status_bar_visible),
),
])
**输入区(Input Area)** :用户输入提示符和文本的地方。它显示 ``❯ `` 提示符号(可通过皮肤系统自定义),
并支持多行输入。当有图片附件时,会在输入区上方显示附件徽章(如 ``[📎 Image #1]``)。
**Spinner 区域** :Agent 运行时显示的动态等待指示器。它会显示当前工具名称、执行时间、
以及随机出现的可爱表情。当 Agent 空闲时,此区域高度为 0,自动隐藏。
**状态栏(Status Bar)** :显示当前模型名称、上下文使用百分比、会话时长等关键信息。
状态栏的可见性可以通过 ``/statusbar`` 命令切换。
此外,布局还包含一个 ``CompletionsMenu``——当用户按下 Tab 键时,会弹出一个浮动的补全菜单,
显示匹配的 slash 命令。
KeyBindings 键绑定
--------------------
Hermes 注册了丰富的键绑定来支持各种交互模式:
**Enter 键** :根据当前状态有三种行为——
1. 正常模式:提交用户输入
2. Agent 运行中(interrupt 模式):中断当前 Agent 执行
3. 交互模式(clarify/approval/sudo/secret):确认当前选择
**Ctrl+C 键** :五级优先级的中断处理(详见后文)。
**Tab 键** :触发 slash 命令补全。
**方向键(上/下)** :在 clarify 模式中导航选项,在正常模式中浏览历史。
**Ctrl+B** :切换语音录制模式。
Completer 补全器
------------------
``SlashCommandCompleter`` 是一个 ``prompt_toolkit.Completer`` 子类,
它从 ``COMMAND_REGISTRY`` 中获取所有已注册的命令,并提供前缀匹配补全。
补全逻辑:
1. 当用户输入以 ``/`` 开头时,激活命令补全模式
2. 收集所有命令名称和别名,进行前缀匹配
3. 匹配结果按字母排序,显示命令描述作为 meta 信息
4. 对于有子命令的命令(如 ``/reasoning``),在空格后提供子命令补全
5. 集成 skill 命令——通过 ``skill_commands_provider`` 回调动态获取可用的 skill 列表
AutoSuggest 自动建议
----------------------
``SlashCommandAutoSuggest`` 基于 ``FileHistory`` 提供历史输入建议。
当用户开始输入时,它会查找最匹配的历史记录并显示为灰色提示文本。
用户只需按右箭头键即可接受建议。
输入路由优先级
================
Hermes CLI 的输入处理遵循一个 **九级优先级** 系统。当用户按下 Enter 键时,
输入会被依次检查,直到匹配到某个处理器:
.. mermaid::
:name: cli-input-routing
:caption: 输入路由优先级
flowchart TD
INPUT["用户按 Enter"] --> P1{"Level 1: sudo_state?"}
P1 -->|是| SUDO["提交 sudo 密码"]
P1 -->|否| P2{"Level 2: secret_state?"}
P2 -->|是| SECRET["提交 secret 值"]
P2 -->|否| P3{"Level 3: approval_state?"}
P3 -->|是| APPROVAL["确认审批选择"]
P3 -->|否| P4{"Level 4: model_picker?"}
P4 -->|是| MODEL["确认模型选择"]
P4 -->|否| P5{"Level 5: clarify_state?"}
P5 -->|是| CLARIFY["确认 clarify 选择
或提交自由文本"]
P5 -->|否| P6{"Level 6: 空输入?"}
P6 -->|是| IGNORE["忽略空输入"]
P6 -->|否| P7{"Level 7: Agent 运行中?"}
P7 -->|是 + interrupt 模式| INTERRUPT["中断 Agent"]
P7 -->|是 + queue 模式| QUEUE["排队等待"]
P7 -->|否| P8{"Level 8: _pending_input?"}
P8 -->|有| PENDING["从队列取出输入"]
P8 -->|无| P9{"Level 9: 正常输入"}
P9 --> SLASH{"是 Slash 命令?"}
SLASH -->|是| CMD["执行命令"]
SLASH -->|否| AGENT["发送给 Agent"]
这个设计确保了无论 Agent 处于什么状态,用户的紧急交互(密码输入、命令审批)
总是能被正确路由,不会被遗漏或阻塞。
各级别详细说明
----------------
**Level 1 — Sudo** :当 ``terminal_tool`` 需要提升权限时,CLI 进入 sudo 输入模式。
输入会被直接路由到 sudo 密码回调,不会经过任何其他处理。密码以隐藏形式显示。
**Level 2 — Secret** :类似 sudo,用于安全地输入 API Key 等敏感信息。
通过 ``save_env_value_secure()`` 存储到 ``~/.hermes/.env`` ,从不暴露给模型。
**Level 3 — Approval** :当 Agent 尝试执行危险命令时,CLI 显示审批 UI。
用户可以选择 ``once`` (本次允许)、``session`` (本次会话允许)、``always`` (永久允许)、``deny`` (拒绝)。
**Level 4 — Model Picker** :``/model`` 命令触发的模型选择 UI。用户可以通过方向键导航可用模型列表。
**Level 5 — Clarify** :当 Agent 需要用户澄清时,显示选择题界面。
支持方向键导航和 "Other" 选项(自由文本输入)。超时后 Agent 自行决定。
**Level 6 — 空输入** :纯空白输入被直接忽略,不触发任何操作。
**Level 7 — Agent 运行中** :取决于 ``busy_input_mode`` 配置——
- ``interrupt`` (默认):Enter 键中断当前 Agent 执行
- ``queue`` :输入被排队,等待 Agent 完成后自动发送
**Level 8 — 待处理输入队列** :某些命令(如 ``/queue`` 、``/steer``)会将消息放入 ``_pending_input`` 队列。
**Level 9 — 正常输入** :检查是否以 ``/`` 开头(slash 命令),否则作为普通消息发送给 Agent。
Slash 命令系统
================
Hermes 的 slash 命令系统采用 **声明式注册** 架构,通过一个中心化的注册表管理所有命令。
CommandDef 数据类
-------------------
每个命令由 ``CommandDef`` 数据类定义:
.. code-block:: python
@dataclass(frozen=True)
class CommandDef:
name: str # 命令名(不含斜杠)
description: str # 人类可读描述
category: str # 分类:Session, Configuration 等
aliases: tuple[str, ...] = () # 别名
args_hint: str = "" # 参数提示
subcommands: tuple[str, ...] = () # Tab 补全子命令
cli_only: bool = False # 仅 CLI 可用
gateway_only: bool = False # 仅网关可用
gateway_config_gate: str | None = None # 配置门控
命令注册表(COMMAND_REGISTRY)包含约 50 个命令,按功能分为六大类:
.. list-table:: 命令分类概览
:header-rows: 1
:widths: 20 60 20
* - 分类
- 代表命令
- 数量
* - Session
- /new, /clear, /history, /save, /retry, /undo, /title, /branch, /compress, /rollback, /stop
- ~18
* - Configuration
- /model, /provider, /personality, /skin, /yolo, /reasoning, /fast, /voice
- ~10
* - Tools & Skills
- /tools, /skills, /cron, /reload, /reload-mcp, /browser, /plugins
- ~8
* - Info
- /help, /usage, /insights, /copy, /paste, /image, /debug
- ~8
* - Exit
- /quit, /exit
- 1
命令解析流程
--------------
.. mermaid::
:name: cli-slash-resolution
:caption: Slash 命令解析流程
sequenceDiagram
autonumber
participant User as 用户
participant PT as prompt_toolkit
participant CR as COMMAND_REGISTRY
participant Handler as 命令处理器
participant Agent as AIAgent
User->>PT: 输入 /bg some task
PT->>PT: Tab 补全(如果按下)
Note over PT: SlashCommandCompleter
匹配 /background
User->>PT: 按 Enter
PT->>CR: resolve_command("bg")
alt 命令名匹配
CR-->>PT: CommandDef(name="background", aliases=("bg",))
else 命令名不匹配
CR-->>PT: None → 作为普通文本发送
end
PT->>Handler: 执行 background 命令
Handler->>Agent: 创建后台任务
Handler-->>PT: 返回结果
别名解析机制
--------------
``resolve_command()`` 函数处理命令名解析:
1. 去除输入中的前导斜杠(``/``)
2. 转换为小写
3. 在 ``_COMMAND_LOOKUP`` 字典中查找
这个查找表在模块导入时由 ``_build_command_lookup()`` 构建,
它为每个命令的主名和所有别名创建映射。例如:
.. code-block:: python
_COMMAND_LOOKUP = {
"background": CommandDef(name="background", ...),
"bg": CommandDef(name="background", ...), # alias
"exit": CommandDef(name="quit", ...), # alias
"q": CommandDef(name="queue", ...), # alias
"snap": CommandDef(name="snapshot", ...), # alias
}
这样,用户输入 ``/bg some task`` 和 ``/background some task`` 会被路由到同一个处理器。
Tab 补全实现
--------------
``SlashCommandCompleter`` 实现了 ``prompt_toolkit.Completer`` 接口:
1. 解析当前输入,提取出命令名部分和参数部分
2. 如果只有命令名(无空格),提供命令名补全
3. 如果已有空格,检查是否有 ``subcommands`` 定义,提供子命令补全
4. 额外集成 skill 命令——通过 ``skill_commands_provider`` 动态获取可用的 skill 列表
补全菜单的样式由皮肤系统控制——``completion_menu_bg`` 、``completion_menu_current_bg`` 等颜色键。
皮肤/主题引擎
===============
皮肤引擎是 Hermes CLI 最独特的视觉特性之一。它允许用户通过一个 YAML 文件
完全自定义 CLI 的外观,无需修改任何代码。
SkinConfig 数据结构
---------------------
``SkinConfig`` 是皮肤配置的核心数据类:
.. code-block:: python
@dataclass
class SkinConfig:
name: str
description: str = ""
colors: Dict[str, str] = field(default_factory=dict) # 20+ 颜色键
spinner: Dict[str, Any] = field(default_factory=dict) # Spinner 自定义
branding: Dict[str, str] = field(default_factory=dict) # 品牌文案
tool_prefix: str = "┊" # 工具输出前缀
tool_emojis: Dict[str, str] = field(default_factory=dict) # 工具表情覆盖
banner_logo: str = "" # Rich 标记 ASCII 艺术
banner_hero: str = "" # Rich 标记英雄图案
内置主题
----------
Hermes 提供了 8+ 个内置主题,每个都有独特的视觉风格:
.. list-table:: 内置皮肤一览
:header-rows: 1
:widths: 15 30 25 30
* - 名称
- 描述
- 色调
- 特殊元素
* - default
- 经典 Hermes 金色/可爱
- 金色 #FFD700 / 青铜色
- Kawaii 表情、蛇杖图案
* - ares
- 战神主题 — 深红与青铜
- 深红 #9F1C1C / 青铜 #C7A96B
- ⚔ Spinner翅膀、战神ASCII
* - mono
- 单色灰度
- 灰度 #555555 ~ #e6edf3
- 简洁专业
* - slate
- 冷蓝开发者
- 蓝色 #4169e1 / #7eb8f6
- 技术风格
* - daylight
- 亮色主题(浅色终端)
- 蓝色 #2563EB / 深色文字
- 完整亮色UI
* - warm-lightmode
- 暖棕/金色(浅色终端)
- 棕色 #5C3D11 / 金色
- 温暖舒适
* - poseidon
- 海神主题 — 深蓝与海沫
- 蓝色 #2A6FB9 / 海沫 #A9DFFF
- Ψ 三叉戟图案
* - sisyphus
- 西西弗斯主题 — 严谨灰度
- 灰度 #4A4A4A ~ #F5F5F5
- 巨石 ASCII、坚持语录
* - charizard
- 喷火龙主题 — 火山橙
- 橙色 #C75B1D / 琥珀 #FFD39A
- ✦ 火焰图案
颜色键系统
------------
皮肤系统定义了 20+ 个颜色键,每个键控制 UI 中的一个特定元素:
.. list-table:: 主要颜色键
:header-rows: 1
:widths: 25 35 40
* - 颜色键
- 控制元素
- 默认值
* - banner_border
- 横幅边框
- #CD7F32(青铜色)
* - banner_title
- 横幅标题文字
- #FFD700(金色)
* - ui_accent
- 通用 UI 强调色
- #FFBF00(琥珀色)
* - ui_ok / ui_error / ui_warn
- 成功/错误/警告指示器
- 绿/红/橙
* - prompt
- 输入提示文字颜色
- #FFF8DC(乳白色)
* - input_rule
- 输入区分隔线
- #CD7F32
* - response_border
- 响应框边框
- #FFD700
* - status_bar_bg
- 状态栏背景色
- #1a1a2e(深蓝黑)
* - completion_menu_*
- 补全菜单(4个键)
- 深色背景
继承机制
----------
.. mermaid::
:name: cli-skin-inheritance
:caption: 皮肤引擎继承机制
classDiagram
class SkinConfig {
+name: str
+description: str
+colors: Dict
+spinner: Dict
+branding: Dict
+tool_prefix: str
+tool_emojis: Dict
+banner_logo: str
+banner_hero: str
+get_color(key, fallback) str
+get_spinner_wings() List
+get_branding(key, fallback) str
}
class DefaultSkin {
金色/青铜色配色
Kawaii spinner 表情
Hermes 品牌
}
class UserSkin {
仅覆盖部分颜色键
其余从 default 继承
}
class AresSkin {
深红/青铜配色
自定义 spinner 翅膀
战神品牌
}
DefaultSkin <|-- AresSkin : 完整定义
DefaultSkin <|-- UserSkin : 部分覆盖
note for UserSkin "_build_skin_config() 将
default 的值作为基础,
然后用用户的值覆盖"
note for SkinConfig "所有字段都是可选的。
缺失值自动继承 default 皮肤。"
``_build_skin_config()`` 函数实现了继承逻辑:
1. 以 ``default`` 皮肤的值为基础
2. 用用户提供的值覆盖对应字段
3. 返回一个完整的 ``SkinConfig`` 实例
这意味着用户只需要定义想要修改的颜色,其余自动继承。例如,
一个只修改了 ``banner_border`` 和 ``prompt_symbol`` 的用户皮肤:
.. code-block:: yaml
name: mytheme
description: 只改了边框和提示符
colors:
banner_border: "#FF00FF"
branding:
prompt_symbol: ">>> "
其余 20+ 个颜色键会自动使用 ``default`` 皮肤的值。
prompt_toolkit 样式桥接
-------------------------
``get_prompt_toolkit_style_overrides()`` 函数将皮肤颜色键映射为
prompt_toolkit 的样式类名。例如:
.. code-block:: python
{
"input-area": prompt, # 输入区文字颜色
"status-bar": f"bg:{status_bg} {text}", # 状态栏背景+前景
"clarify-selected": f"{title} bold", # Clarify 选中项
"approval-title": f"{warn} bold", # Approval 标题
"sudo-prompt": f"{error} bold", # Sudo 提示
"completion-menu": f"bg:{menu_bg} {text}",# 补全菜单
}
这套桥接机制确保 ``/skin`` 命令切换后,TUI 的所有元素(包括补全菜单、交互 UI)
都会立即反映新的配色方案。
KawaiiSpinner
===============
``KawaiiSpinner`` 是 Hermes CLI 的标志性 UI 组件——一个带有可爱表情的动画等待指示器。
9 种动画风格
--------------
Spinner 支持以下动画类型,每种都有独特的视觉节奏:
.. list-table:: Spinner 动画风格
:header-rows: 1
:widths: 15 40 30
* - 名称
- 帧序列
- 视觉效果
* - dots
- ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
- 经典 braille 旋转
* - bounce
- ⠁⠂⠄⡀⢀⠠⠐⠈
- 弹跳线条
* - grow
- ▁▂▃▄▅▆▇█▇▆▅▄▃▂
- 生长柱状图
* - arrows
- ←↖↑↗→↘↓↙
- 箭头旋转
* - star
- ✶✷✸✹✺✹✸✷
- 星形闪烁
* - moon
- 🌑🌒🌓🌔🌕🌖🌗🌘
- 月相变化
* - pulse
- ◜◠◝◞◡◟
- 脉冲圆弧
* - brain
- 🧠💭💡✨💫🌟💡💭
- 大脑思维
* - sparkle
- ⁺˚*✧✦✧*˚
- 闪烁星光
Kawaii 表情
-------------
除了动画帧,Spinner 还定义了两套表情:
**等待表情(KAWAII_WAITING)** :
.. code-block:: python
"(。◕‿◕。)", "(◕‿◕✿)", "٩(◕‿◕。)۶", "(✿◠‿◠)", "( ˘▽˘)っ",
"♪(´ε` )", "(◕ᴗ◕✿)", "ヾ(^∇^)", "(≧◡≦)", "(★ω★)"
**思考表情(KAWAII_THINKING)** :
.. code-block:: python
"(。•́︿•̀。)", "(◔_◔)", "(¬‿¬)", "( •_•)>⌐■-■", "(⌐■_■)",
"(´・_・`)", "◉_◉", "(°ロ°)", "( ˘⌣˘)♡", "ヽ(>∀<☆)☆"
**思考动词(THINKING_VERBS)** :
.. code-block:: python
"pondering", "contemplating", "musing", "cogitating", "ruminating",
"deliberating", "mulling", "reflecting", "processing", "reasoning",
"analyzing", "computing", "synthesizing", "formulating", "brainstorming"
这些表情和动词都支持通过皮肤系统自定义。例如,``ares`` 主题使用战神风格的动词:
.. code-block:: python
"forging", "marching", "sizing the field", "holding the line",
"hammering plans", "tempering steel", "plotting impact", "raising the shield"
线程安全渲染
--------------
``KawaiiSpinner`` 在一个独立的守护线程中运行动画循环:
.. code-block:: python
def start(self):
self.running = True
self.start_time = time.time()
self.thread = threading.Thread(target=self._animate, daemon=True)
self.thread.start()
动画循环每 0.12 秒刷新一帧,使用 ``\r`` 回车符覆盖当前行(而不是打印新行),
这样 spinner 始终保持在同一行。通过 ``self.last_line_len`` 记录上一行的长度,
在写入新帧时用空格填充差值,确保旧内容被完全覆盖。
``print_above()`` 方法允许在 spinner 上方打印文本而不破坏动画:
先清除 spinner 行,打印文本,然后让下一帧重新绘制 spinner。
环境适配
----------
Spinner 会根据运行环境自动调整行为:
**TTY 检测** :通过 ``self._out.isatty()`` 判断标准输出是否连接到真正的终端。
如果输出被重定向到文件或管道(Docker、systemd),跳过所有动画,
只打印一行静态的 ``[tool] message`` 和 ``[done] message`` 。
**prompt_toolkit StdoutProxy 检测** :当运行在 ``patch_stdout()`` 上下文中时,
``sys.stdout`` 被 prompt_toolkit 的 ``StdoutProxy`` 包装,它会在每次刷新时注入换行符,
导致 ``\r`` 覆盖失败——每个 spinner 帧都出现在新行上。在这种情况下,spinner 退化为
一个空循环(``time.sleep(0.1)``),让 TUI 的 ``_spinner_text`` 小部件接管显示。
工具预览系统
==============
当 Agent 调用工具时,CLI 需要在紧凑的一行中展示工具调用的关键信息。
这就是 ``build_tool_preview()`` 和 ``get_cute_tool_message()`` 的工作。
build_tool_preview()
----------------------
这个函数接受工具名和参数字典,返回一个简短的预览字符串:
.. list-table:: 工具预览示例
:header-rows: 1
:widths: 20 40 30
* - 工具名
- 参数
- 预览输出
* - terminal
- {"command": "npm test"}
- npm test
* - web_search
- {"query": "python async"}
- python async
* - read_file
- {"path": "/src/main.py"}
- /src/main.py
* - write_file
- {"path": "/src/main.py"}
- /src/main.py
* - search_files
- {"pattern": "TODO", "target": "content"}
- TODO
* - memory
- {"action": "add", "target": "user", "content": "likes cats"}
- +user: "likes cats"
* - todo
- {"todos": [...], "merge": false}
- planning 3 task(s)
对于没有专门处理逻辑的工具,函数会尝试从一组候选参数键(``query``, ``text``,
``command``, ``path``, ``name``, ``prompt``, ``code``, ``goal``)中提取预览文本。
get_cute_tool_message()
-------------------------
这个函数生成工具完成后的格式化输出行:
.. code-block:: text
┊ 🔍 search python async 2.3s
┊ 💻 $ npm test 1.5s
┊ 📖 read /src/main.py 0.1s
┊ ✍️ write /src/main.py 0.3s
┊ 🔧 patch /src/main.py 0.2s
格式为 ``| {emoji} {verb:9} {detail} {duration}`` ,其中:
- **前缀字符** (``┊``):由皮肤的 ``tool_prefix`` 控制
- **emoji** :由皮肤的 ``tool_emojis`` 覆盖或工具注册表的默认值
- **动词** :左对齐 9 字符(如 ``search``, ``$``, ``read``, ``write``)
- **详情** :截断到 35-42 字符
- **持续时间** :格式化为 ``X.Xs``
工具失败检测
--------------
``_detect_tool_failure()`` 检查工具结果是否表示失败:
- ``terminal`` :检查 ``exit_code`` 是否非零,显示 ``[exit N]``
- ``memory`` :检查是否超出限制,显示 ``[full]``
- 通用:检查 ``"error"`` / ``"failed"`` 关键词,显示 ``[error]``
失败的工具调用会以红色前缀显示。
内联 Diff 系统
================
当 Agent 编辑文件时,Hermes CLI 会在工具输出下方直接显示 **unified diff** 预览,
让用户即时看到文件变更。这是通过 ``LocalEditSnapshot`` 和 ``extract_edit_diff()``
实现的。
LocalEditSnapshot
-------------------
在工具执行 **之前** ,``capture_local_edit_snapshot()`` 会记录目标文件的当前内容:
.. code-block:: python
@dataclass
class LocalEditSnapshot:
paths: list[Path] = field(default_factory=list)
before: dict[str, str | None] = field(default_factory=dict)
支持的工具有:
- ``write_file`` :快照目标路径
- ``patch`` :快照目标路径
- ``skill_manage`` :快照 skill 相关文件(create/edit/patch/write_file/remove_file/delete)
工具执行 **之后** ,``extract_edit_diff()`` 比较快照和当前文件内容,
生成 unified diff。
皮肤感知的 ANSI 渲染
----------------------
diff 的颜色由皮肤系统控制。``_diff_ansi()`` 函数从活跃皮肤中提取颜色:
.. list-table:: Diff 颜色映射
:header-rows: 1
:widths: 15 30 30
* - 元素
- 皮肤颜色键
- 默认值
* - 文件头
- session_label
- 紫色 #180;160;255
* - Hunk 头
- session_border
- 灰色 #120;120;140
* - 删除行(-)
- ui_error(半透明背景)
- 深红背景
* - 新增行(+)
- ui_ok(半透明背景)
- 深绿背景
* - 上下文行
- banner_dim
- 灰色 #150;150;150
diff 输出有截断保护:最多显示 6 个文件、80 行 diff。超出部分会显示省略摘要。
交互模式
==========
Hermes CLI 支持多种交互模式,每种都针对特定的用户交互需求。
Clarify(澄清)
-----------------
当 Agent 需要用户做出选择时,触发 clarify 回调:
.. code-block:: python
def clarify_callback(cli, question, choices):
timeout = CLI_CONFIG.get("clarify", {}).get("timeout", 120)
response_queue = queue.Queue()
is_open_ended = not choices
cli._clarify_state = {
"question": question,
"choices": choices,
"selected": 0,
"response_queue": response_queue,
}
cli._clarify_deadline = time.monotonic() + timeout
# 阻塞等待用户响应
while True:
try:
result = response_queue.get(timeout=1)
return result
except queue.Empty:
if time.monotonic() > cli._clarify_deadline:
break
# 超时:让 Agent 自行决定
return "The user did not provide a response. Use your best judgement."
关键特性:
- **方向键导航** :上下键在选项之间移动,选中项高亮显示
- **"Other" 选项** :用户可以切换到自由文本输入模式
- **超时机制** :默认 120 秒,超时后 Agent 自行决定
- **实时倒计时** :UI 显示剩余时间
Approval(审批)
------------------
当 Agent 尝试执行危险命令时,触发 approval 回调:
.. list-table:: Approval 选项
:header-rows: 1
:widths: 15 30 55
* - 选项
- 效果
- 说明
* - once
- 本次允许
- 仅当前命令
* - session
- 本次会话允许
- 同一命令在当前会话中不再询问
* - always
- 永久允许
- 写入永久白名单
* - deny
- 拒绝
- 不执行命令
* - view(可选)
- 查看完整命令
- 命令超过 70 字符时自动出现
approval 回调使用 ``_approval_lock`` 序列化并发请求(例如来自并行委派子任务的请求),
确保每个提示都有自己的处理轮次。
Secret(密码输入)
--------------------
Secret 输入有两种模式:
**TUI 模式** (有 ``_app``):通过 prompt_toolkit 的 ``PasswordProcessor`` 隐藏输入,
``response_queue`` 阻塞等待。输入缓冲区在进入密码模式前被清空,
防止残留的草稿被误提交。
**Fallback 模式** (无 ``_app``):使用标准库的 ``getpass.getpass()`` 。
Secret 值通过 ``save_env_value_secure()`` 存储到 ``~/.hermes/.env`` ,
**从不暴露给模型** 。
状态栏三层自适应
==================
状态栏是 CLI 底部的一行信息条,显示模型名称、上下文使用量和会话时长。
它有三层自适应布局:
.. list-table:: 状态栏布局
:header-rows: 1
:widths: 15 45 40
* - 层级
- 终端宽度
- 显示内容
* - Narrow
- < 52 列
- ``⚕ model · 3m``
* - Medium
- 52 - 75 列
- ``⚕ model · 45% · 3m 12s``
* - Wide
- >= 76 列
- ``⚕ model · ctx ████████░░ 45% · in:12k out:8k · $0.42 · 3m 12s``
Wide 布局额外显示:
- **上下文进度条** :``████████░░`` 可视化上下文使用率
- **Token 统计** :输入 token、输出 token
- **费用估算** :基于当前模型的定价
- **压缩次数** :上下文被压缩的次数
颜色编码
----------
上下文使用率的颜色编码:
.. list-table:: 上下文颜色
:header-rows: 1
:widths: 20 30 30
* - 使用率
- 样式类
- 视觉含义
* - < 50%
- status-bar-good
- 绿色:充裕
* - 50% - 80%
- status-bar-warn
- 黄色:注意
* - 80% - 95%
- status-bar-bad
- 橙色:紧张
* - >= 95%
- status-bar-critical
- 红色:危急
Ctrl+C 分级处理
=================
Ctrl+C 在 Hermes CLI 中有五级优先级的处理逻辑:
.. list-table:: Ctrl+C 五级处理
:header-rows: 1
:widths: 10 30 30 30
* - 级别
- 条件
- 行为
- 目的
* - 1
- 在交互模式(clarify/approval/sudo/secret)中
- 取消当前交互,返回默认值
- 允许用户退出不想回答的提示
* - 2
- Agent 正在运行
- 中断 Agent 执行(设置 ``_should_exit`` 标志)
- 停止正在进行的工具调用
* - 3
- 空闲状态,距离上次 Ctrl+C < 1 秒
- 退出 REPL(设置 ``_should_exit = True``)
- 双击 Ctrl+C 快速退出
* - 4
- 空闲状态,距离上次 Ctrl+C < 3 秒
- 显示确认退出提示
- 防止误触
* - 5
- 其他情况
- 记录时间戳,不做任何事
- 重置超时计时
这个分级系统确保了:
- 在 **紧急情况下** (Agent 运行中),第一次 Ctrl+C 就能中断执行
- 在 **空闲状态下** ,需要双击才能退出,防止误触
- 在 **交互模式中** ,Ctrl+C 优雅地取消当前提示,而不是退出整个程序
扩展 CLI 命令生态
==================
随着 Hermes Agent 功能的不断丰富,CLI 层面陆续引入了多个重量级子系统。
它们不再只是简单的 slash 命令,而是拥有独立数据模型、持久化存储和复杂交互逻辑的 **自治模块** 。
本节将逐一剖析这些扩展命令的设计理念和架构要点。
Kanban 看板系统
-----------------
``hermes kanban`` 是一个功能完备的任务管理看板,由三个核心模块支撑:
- ``hermes_cli/kanban.py`` (约 2000 行)——看板 CLI 的主入口和交互逻辑
- ``hermes_cli/kanban_db.py`` (约 4000 行)——基于 SQLite 的持久化存储层
- ``hermes_cli/kanban_diagnostics.py`` (约 650 行)——任务健康诊断引擎
**核心能力** :
- **任务生命周期管理** :创建(``hermes kanban create``)、分配(``assign``)、
状态流转(``move``)、关闭(``close``)——完整的任务生命周期覆盖。
- **看板仪表盘** :以列式布局展示各阶段任务,支持按优先级、负责人、标签筛选。
仪表盘通过 WebSocket 推送实时更新,无需手动刷新。
- **诊断引擎** :``kanban_diagnostics.py`` 实现了一套任务"压力信号"检测机制——
当任务长时间未推进、依赖阻塞或被反复重开时,系统会主动发出告警,
帮助团队及时发现和化解瓶颈。
.. mermaid::
:name: kanban-architecture
:caption: Kanban 看板系统架构
flowchart TD
CLI["hermes kanban "] --> CMD["kanban.py
命令解析与交互"]
CMD --> DB["kanban_db.py
SQLite 持久化"]
CMD --> DIAG["kanban_diagnostics.py
健康诊断"]
DB --> SQLITE["~/.hermes/kanban.db"]
DIAG --> SIGNALS["压力信号检测"]
SIGNALS --> NOTIFY["告警通知"]
Curator 内容管理
------------------
``hermes curator`` 提供了两个子命令,用于管理 Skill 和捆绑内容的生命周期:
- ``hermes curator archive`` ——将不再活跃的 Skill 归档,释放索引空间但保留历史记录
- ``hermes curator prune`` ——清理过期或冗余的内容条目
Curator 的设计哲学是 **轻量但有序** :Agent 的 Skill 仓库会随着时间膨胀,
如果没有定期整理机制,搜索和加载效率都会下降。
Curator 通过标签和最后使用时间自动判断哪些内容值得保留、哪些可以归档或删除。
Voice 语音系统
----------------
``hermes_cli/voice.py`` (约 734 行)实现了完整的语音输入输出能力:
- **语音输入(STT)** :支持多种语音识别提供者,将用户语音实时转写为文本
- **语音输出(TTS)** :支持多种 TTS 引擎,可将 Agent 回复合成为语音播放
- **豆包语音集成** :内置对字节跳动 Doubao Speech 的支持,提供低延迟的中文语音识别与合成
语音模式可通过 ``/voice`` slash 命令或 ``Ctrl+B`` 快捷键切换。
每个语音提供者独立配置,用户可以在 ``config.yaml`` 中选择偏好的 TTS/STT 引擎。
Hooks 钩子系统
-----------------
``hermes_cli/hooks.py`` (约 385 行)管理 Shell 钩子,让用户可以将 Hermes 的行为
自动化嵌入到 Shell 工作流中:
- **会话生命周期钩子** :在会话开始、结束时自动执行预定义脚本
- **事件触发钩子** :响应 Agent 特定事件(如工具调用完成、任务状态变更)
- **注册与管理** :通过 ``hermes hooks add`` / ``hermes hooks remove`` / ``hermes hooks list``
管理已注册的钩子
钩子系统的核心思想是让 Agent 的能力 **延伸到终端环境**——
例如,在每次会话结束时自动推送摘要到 Slack,或在特定工具调用后触发文件同步。
Goals 目标追踪
-----------------
``hermes_cli/goals.py`` (约 535 行)实现了一个目标追踪与进度管理系统:
- **目标创建** :定义长期或短期目标,关联里程碑和截止日期
- **进度追踪** :自动或手动更新目标的完成进度
- **聚合视图** :以 dashboard 形式展示所有活跃目标的状态
目标系统与 Kanban 看板形成互补——Kanban 管理具体任务的流转,
Goals 则从更高维度追踪战略目标的推进情况。
Onboarding 新手引导
----------------------
``agent/onboarding.py`` 为首次使用 Hermes 的用户提供交互式引导流程:
- **环境检测** :自动检查 Python 版本、必要的依赖、API Key 配置
- **交互式配置** :引导用户完成 ``config.yaml`` 的关键配置项
- **快速体验** :配置完成后自动启动一次示范会话,让用户快速上手
新手引导在用户首次执行 ``hermes`` 命令时自动触发,
也支持通过 ``hermes setup`` 手动重新运行。
Account Usage 用量追踪
-------------------------
``agent/account_usage.py`` 提供跨 Provider 的 API 用量追踪能力:
- **多 Provider 聚合** :统一展示来自 Anthropic、OpenAI、OpenRouter 等不同提供商的消费
- **模型级别统计** :按模型拆分 token 用量和费用估算
- **用量预警** :接近配额上限时主动提醒
通过 ``/usage`` slash 命令可以在 REPL 中实时查看当前会话和历史会话的用量统计。
其他 CLI 改进
--------------
除了上述重量级子系统外,CLI 层面还有若干值得关注的改进:
**Slack CLI** (``hermes_cli/slack_cli.py``):提供 Slack 集成的专用命令,
支持从终端直接向 Slack 频道发送消息或拉取对话上下文。
**Fallback 命令** (``hermes_cli/fallback_cmd.py``):管理后备 Provider 的配置。
当主 Provider 不可用时,系统自动切换到预配置的备选方案,确保服务连续性。
**Skills Reset** (``hermes skills reset``):新增的子命令,
允许用户将 Skill 回退到默认状态,清除自定义修改。
**Model Catalog 改进** (``hermes_cli/model_catalog.py``):
引入 ``list_picker_providers`` 函数和基于 ``config.yaml`` 的模型别名解析机制,
使 ``/model`` 命令的模型列表更加智能——自动识别用户配置的别名,
按 Provider 分组展示,并标记推荐模型。
**Startup Tips** :新增约 100 条 CLI 启动提示,
在 REPL 启动时随机展示,涵盖快捷键技巧、隐藏功能、最佳实践等。
源码文件索引
==============
本章涉及的主要源文件:
- ``cli.py`` — ``HermesCLI`` 类,REPL 主循环,输入路由,状态栏,Ctrl+C 处理
- ``hermes_cli/main.py`` — CLI 入口点,argparse 子命令解析,profile 覆盖
- ``hermes_cli/commands.py`` — ``CommandDef`` 数据类,``COMMAND_REGISTRY`` ,别名解析,Tab 补全
- ``hermes_cli/callbacks.py`` — Clarify、Secret、Approval 交互回调
- ``hermes_cli/skin_engine.py`` — ``SkinConfig`` ,8+ 内置主题,颜色继承,prompt_toolkit 样式桥接
- ``agent/display.py`` — ``KawaiiSpinner`` ,工具预览,内联 diff
- ``hermes_cli/kanban.py`` — Kanban 看板 CLI 入口,任务管理交互
- ``hermes_cli/kanban_db.py`` — Kanban SQLite 持久化存储层
- ``hermes_cli/kanban_diagnostics.py`` — 任务健康诊断引擎
- ``hermes_cli/curator.py`` — Curator 内容归档与清理
- ``hermes_cli/voice.py`` — 语音输入输出系统(STT/TTS)
- ``hermes_cli/hooks.py`` — Shell 钩子注册与管理
- ``hermes_cli/goals.py`` — 目标追踪与进度管理
- ``hermes_cli/slack_cli.py`` — Slack 集成命令
- ``hermes_cli/fallback_cmd.py`` — 后备 Provider 管理
- ``hermes_cli/model_catalog.py`` — 模型目录与别名解析
- ``agent/onboarding.py`` — 新手引导流程
- ``agent/account_usage.py`` — 跨 Provider 用量追踪