Ink TUI 全新终端界面详解
概述
Ink TUI 是 Hermes Agent 的全新现代化终端界面,用 Node.js 构建,运行在 Python CLI 的子进程里。与传统 CLI 共用同一套运行时、会话、slash 命令和配置,但提供了更流畅、更丰富的交互体验。
一句话总结:hermes --tui 启动的那个漂亮的终端界面,就是 Ink TUI。
1. 架构全貌
进程关系
用户终端(Terminal)
│
▼
┌─────────────────────────────────────┐
│ Python CLI(hermes --tui 进程) │
│ - 参数解析、配置加载、凭证管理 │
│ - 启动 Node.js TUI 子进程 │
│ - 启动 tui_gateway 子进程(JSON-RPC)│
└──────────┬──────────────────────────┘
│ stdio(JSON-RPC)
▼
┌─────────────────────────────────────┐
│ tui_gateway(Python 子进程) │
│ - JSON-RPC 命令分发器 │
│ - 处理 tool_call / reasoning / 状态 │
│ - 渲染器桥接(agent.rich_output) │
└──────────┬──────────────────────────┘
│ JSON-RPC
▼
┌─────────────────────────────────────┐
│ Ink TUI(Node.js 进程) │
│ - 终端渲染(ANSI 输出) │
│ - 用户输入捕获 │
│ - 模态浮层、鼠标选择、主题切换 │
└─────────────────────────────────────┘
关键文件:
tui_gateway/entry.py— gateway 入口,从 stdin 读取 JSON-RPC 命令,写入 stdouttui_gateway/server.py— 分发器(161KB),处理所有 JSON-RPC 请求tui_gateway/transport.py— 传输层抽象(StdioTransport / TeeTransport)tui_gateway/event_publisher.py— WebSocket 事件发布(dashboard sidebar 用)tui_gateway/render.py— 渲染器桥接(调用agent.rich_output)
2. 启动方式
# 标准启动
hermes --tui
# 恢复上次会话
hermes --tui -c
hermes --tui --continue
# 恢复指定会话
hermes --tui -r 20260409_000000_aa11bb
hermes --tui --resume "my session name"
# 开发模式(跳过预构建)
hermes --tui --dev
# 环境变量方式(全局启用)
export HERMES_TUI=1
hermes # 自动走 TUI
hermes chat # 同上
3. 核心特性
3.1 即时首帧渲染
TUI 的 banner(启动画面)在应用完全加载前就开始绘制,终端永远不会感觉"冻住"了。
3.2 非阻塞输入
可以在会话就绪前就开始打字和排队消息。消息会在 agent 上线的瞬间立即发送,不会丢失。
3.3 丰富的模态浮层
以下 slash 命令在 TUI 中以模态浮层(modal overlay)呈现,而不是内联流程:
| 命令 | TUI 行为 |
|---|---|
/help |
分类命令列表,方向键导航 |
/sessions |
模态会话选择器 — 预览、标题、token 统计、在线恢复 |
/model |
按 Provider 分组的模型选择器,含费用提示 |
/skin |
实时预览 — 浏览时主题实时切换 |
/details |
切换详细的 tool-call 详情(全局或按分区) |
/usage |
丰富的 token / 费用 / 上下文面板 |
3.4 鼠标友好选择
拖拽高亮文本使用统一的背景色,而不是 SGR 反转。与终端的正常复制手势一致,体验接近 GUI 应用。
3.5 无闪烁流式渲染
差分更新意味着流式输出时不会闪烁,退出后也不会有滚动条残留(alternate-screen 渲染模式)。
3.6 粘贴与附件
- 内联粘贴折叠长代码片段
Cmd+V/Ctrl+V优先普通文本粘贴,回退到 OSC52/剪贴板读取- 剪贴板图像自动识别并作为附件附加
- 文件路径自动规范化
3.7 实时状态行
TUI 的状态行实时跟踪 agent 状态:
| 状态 | 含义 |
|---|---|
starting agent… |
会话已建立,工具和技能还在初始化。可以打字,消息会排队等待 |
ready |
Agent 空闲,接受输入 |
thinking… / running… |
Agent 正在推理或运行工具 |
interrupted |
当前轮次已取消,按 Enter 重新发送 |
forging session… / resuming… |
首次连接或 --resume 握手中 |
4. 分区展示(Details Mode)
TUI 将对话流渲染为分区分段(sections),默认展开 thinking 和 tools:
| 分区 | 默认可见性 | 说明 |
|---|---|---|
thinking |
展开 | 推理过程实时流式展示 |
tools |
展开 | 工具调用和结果实时展示 |
subagents |
折叠(chevron) | 仅在发生实际委托时显示 |
activity |
隐藏 | 背景元数据(gateway 提示、通知),大多数日常使用中是噪音 |
可通过配置或 /details 命令动态调整:
display:
details_mode: collapsed # hidden | collapsed | expanded
sections:
thinking: expanded # 强制展开
tools: expanded # 强制展开
activity: collapsed # 开启活动面板
运行时命令:
/details [hidden|collapsed|expanded|cycle] # 全局
/details <section> [hidden|collapsed|expanded] # 单区
5. 主题(Skins)
TUI 支持完整的皮肤/主题系统,与经典 CLI 共享同一套配置:
display:
skin: default # 内置或自定义皮肤
运行时切换:
/skin ares # 实时预览,浏览时主题即时切换
支持自定义的元素:banner 色调、UI 颜色、提示符 glyph/颜色、会话显示、命令补全菜单、选择背景、tool_prefix、help_header。
6. JSON-RPC 通信协议
tui_gateway 通过 JSON-RPC 2.0 与 TUI 通信。
Gateway 入口(entry.py)
# 读取 stdin 的每一行 JSON-RPC 命令
for raw in sys.stdin:
req = json.loads(raw.strip())
resp = dispatch(req) # 分发到对应 handler
write_json(resp) # 写入 stdout
事件类型
Gateway 主动推送的事件(method: "event"):
| type | 说明 |
|---|---|
gateway.ready |
启动完成,带 skin 信息 |
gateway.stderr |
Gateway 端的错误输出 |
agent.reasoning |
推理内容片段 |
agent.tool_call |
工具调用请求 |
agent.tool_result |
工具执行结果 |
agent.message |
最终回复文本 |
agent.error |
Agent 层错误 |
渲染器桥接
TUI 本身不直接做 markdown 渲染,而是通过 render.py 调用 Python 端的 agent.rich_output:
# render.py
def render_message(text: str, cols: int = 80) -> str | None:
from agent.rich_output import format_response
return format_response(text, cols=cols)
如果 agent.rich_output 不存在,TUI 回退到自带的 markdown.tsx 渲染。
7. 传输层抽象(Transport Layer)
tui_gateway/transport.py 定义了传输层接口,让同一套分发逻辑可以跑在不同的 I/O 介质上:
@runtime_checkable
class Transport(Protocol):
def write(self, obj: dict) -> bool: ...
def close(self) -> None: ...
三种具体实现:
StdioTransport
写入流(通常是 sys.stdout),用于本地 stdio 模式。加锁保证线程安全,支持 BrokenPipeError 静默处理。
TeeTransport
一主多从的复制模式。主传输的返回值决定结果;从传输静默吞掉异常,永不阻塞主路径。用于 PTY 子进程:每一帧同时发给 stdio(Ink)和 WebSocket(dashboard sidebar)。
server._stdio_transport = TeeTransport(
server._stdio_transport, WsPublisherTransport(url)
)
WsPublisherTransport
通过 WebSocket 将事件镜像到 dashboard 的 /api/pub 端点。后台 daemon 线程异步发送,队列满时直接丢弃(best-effort)。
8. 信号处理(entry.py)
Gateway 专门处理三类信号,防止"静默消失":
| 信号 | 处理方式 |
|---|---|
SIGPIPE |
忽略 — 防止后台线程(TTS、voice)写已关闭的 stdout 导致进程崩溃 |
SIGTERM / SIGHUP |
记录完整栈追踪到 ~/.hermes/logs/tui_gateway_crash.log |
SIGINT |
忽略 — Ctrl+C 留给 TUI 处理 |
崩溃日志同时输出到 stderr,让 TUI 能在 Activity 面板展示。
9. 与经典 CLI 的关系
| 维度 | 经典 CLI | Ink TUI |
|---|---|---|
| 界面 | prompt_toolkit | Node.js 渲染 |
| 交互 | 内联流程 | 模态浮层 |
| 鼠标 | 不支持 | 拖拽选择 |
| 首帧 | 有延迟 | 即时渲染 |
| 输入阻塞 | 阻塞等待 | 非阻塞排队 |
共用:~/.hermes/state.db(同一会话)、slash 命令、配置、~/.hermes/config.yaml、凭证、会话压缩逻辑。
可以混用:在 TUI 里开的会话,关掉后用 hermes(经典 CLI)也能恢复。
10. Web Dashboard 集成
TUI 还可以在浏览器里运行(Web Dashboard 的 Chat 标签页):
Dashboard Server(Node.js)
│
│ POSIX pseudo-terminal
▼
hermes --tui 子进程
│
│ ANSI 输出流
▼
xterm.js(WebGL 渲染)
文档原文:
"The dashboard is running the real TUI binary and rendering its ANSI output through xterm.js with its WebGL renderer for pixel-perfect cell layout."
TUI 的所有功能(slash 命令、模型选择、工具卡片、markdown 流式渲染、skin 主题)在浏览器里完全一致。
11. 适用场景建议
| 场景 | 推荐 |
|---|---|
| 日常交互开发 | ✅ Ink TUI(非阻塞输入、实时状态) |
| 远程服务器(SSH) | ✅ Ink TUI(xterm.js 远程渲染) |
| 无头/批处理 | ❌ 用 hermes chat 或脚本模式 |
| 极端低带宽 | ❌ 经典 CLI(更少的渲染开销) |
| 需要 GUI 拖拽 | ✅ Ink TUI |
相关源文件
tui_gateway/entry.py— Gateway 入口、信号处理、退出日志tui_gateway/server.py— JSON-RPC 分发器(161KB,最核心)tui_gateway/transport.py— Transport ABC(Stdio / Tee / WsPublisher)tui_gateway/event_publisher.py— WebSocket 镜像发布tui_gateway/render.py— Python 渲染器桥接tui_gateway/ws.py— WebSocket 服务器tui_gateway/slash_worker.py— slash 命令后台工作器website/docs/user-guide/tui.md— 官方文档