Skip to content

OpenClaw 源码解读(二十)TUI 终端交互界面

一、导读

TUI(Terminal User Interface)是 OpenClaw 的终端全功能客户端——一个运行在命令行中的交互式聊天界面,使用 @mariozechner/pi-tui(Ink 框架衍生)构建,支持实时流式输出、斜杠命令、模型/Agent/Session 选择器、工具调用实时展示等。

通过 openclaw tui 启动,它通过 WebSocket 连接 Gateway,提供接近 Web UI 的交互体验:

┌─────────────────────────────────────────────┐
│ 🤖 openclaw main › claude-sonnet-4-2025...  │  ← 状态栏
├─────────────────────────────────────────────┤
│ [user] Hello, what can you do?              │
│ [assistant] I can help you with...          │  ← 聊天日志
│ ▸ tool: web_search("latest news")           │  ← 工具调用
│ [assistant] Here are the latest news...     │
├─────────────────────────────────────────────┤
│ > /model ▍                                  │  ← 输入框(支持斜杠命令补全)
└─────────────────────────────────────────────┘

源码位于 src/tui/,约 40 个文件,~4000 行代码。

![TUI 终端交互界面整体架构概览](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/01-infographic-tui-architecture-overview-1775150681188.png)


二、入口 —— tui.ts

startTui() 是 TUI 的主入口函数(~500 行),它的启动流程可以分为 7 个阶段:

阶段 1:配置加载

resolvedGatewayUrl  ← 配置文件 / 命令行参数 / 默认值
sessionKey          ← 参数指定 / Gateway sessions.resolve
agentId             ← 参数指定 / 默认 "main"

阶段 2:Gateway 连接

client = createGatewayChatClient({
  url, sessionKey, password, token, deliver
})
await client.connect()  // WebSocket 握手

GatewayChatClient 封装了 WebSocket 连接、JSON-RPC 请求/响应、事件监听。

阶段 3:TUI 框架初始化

const tui = createTUI({
  input: process.stdin,
  output: process.stdout,
  slashCommands: getSlashCommands({ cfg, provider, model })
})

pi-tui 框架接管 stdin/stdout,提供组件渲染、键盘事件、光标控制。

阶段 4:组件树构建

chatLog      ← ChatLog 组件(消息列表 + 流式更新)
statusLine   ← 状态栏(连接/Agent/模型/Session 信息)
inputHandler ← 输入处理器(消息发送 + 命令分发)
overlay      ← 叠层(模型选择器/Agent 选择器/设置面板)

阶段 5:事件处理器注册

commandHandlers ← 20+ 斜杠命令处理
eventHandlers   ← Gateway 事件路由(chat/agent/presence)

阶段 6:历史加载 + 会话信息

loadHistory()        ← 加载聊天历史
refreshSessionInfo() ← 获取当前会话的 Agent/模型/状态

阶段 7:主循环

tui.start()  // 进入渲染循环
await exitPromise  // 等待退出信号

![startTui() 7 阶段启动流程](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/02-infographic-startup-7-stages-1775150682110.png)


三、GatewayChatClient —— WebSocket 通信

gateway-chat.ts 中的 GatewayChatClient 是 TUI 与 Gateway 之间的通信桥梁:

3.1 核心 RPC 方法

方法Gateway RPC用途
sendMessage()chat.send发送聊天消息
abortRun()chat.abort中止当前 AI 运行
loadHistory()chat.history加载聊天历史
patchSession()sessions.patch修改会话(模型/标签)
listSessions()sessions.list列出所有会话
listModels()models.list列出可用模型
getStatus()status获取 Gateway 状态
listAgents()agents.list列出所有 Agent

3.2 自动重连

与 Web UI 的 WebSocket 客户端类似,TUI 也实现了指数退避重连:

断连 → delay(1s) → 重连 → 成功: 重置延迟
                         → 失败: delay *= 1.5, max 15s

断连时状态栏自动显示 disconnected,重连后自动恢复 session 状态。

![GatewayChatClient WebSocket 通信架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/03-infographic-gateway-websocket-1775150683094.png)


四、流式文本组装 —— TuiStreamAssembler

tui-stream-assembler.ts 是 TUI 的"文本流装配工厂"——它将 Gateway 推送的增量事件装配成完整的显示文本。

4.1 增量处理

Gateway 的 chat.delta 事件携带的是累积全文(不是增量)。TuiStreamAssembler 负责:

typescript
ingestDelta(runId, message, showThinking): string | null {
  // 1. 从 message 中提取 fullText
  // 2. 可选提取 thinking text(推理过程)
  // 3. 根据 showThinking 决定是否拼接思考文本
  // 4. 应用静默令牌过滤(NO_REPLY)
  // 5. 返回显示文本(如果和上次相同则返回 null,跳过渲染)
}

4.2 Final 处理

typescript
finalize(runId, message, showThinking): string {
  // 1. 提取最终完整文本
  // 2. 如果是空消息 → 返回 "(no output)"
  // 3. 如果包含静默令牌 → 剥离
  // 4. 清除该 runId 的状态
}

4.3 多 Run 追踪

Assembler 维护 per-runId 的状态,支持同时追踪多个并发运行(比如主 Agent 和 Sub-Agent 同时输出)。

![TuiStreamAssembler 流式文本组装管线](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/04-infographic-stream-assembly-1775150683948.png)


五、事件处理 —— tui-event-handlers.ts

createEventHandlers() 返回两个事件处理函数:

5.1 Chat 事件

delta   → assembler.ingestDelta() → chatLog.updateAssistant() → setActivityStatus("streaming")
final   → assembler.finalize()    → chatLog.finalizeAssistant() → setActivityStatus("idle")
aborted → chatLog.addSystem("run aborted") → terminateRun()
error   → chatLog.addSystem(errorMessage)   → terminateRun()

精巧的防重复设计:

  • finalizedRuns Map 追踪已完成的 runId(最多 200 条,10 分钟过期)
  • 对已 finalized 的 run 的 delta/final 事件直接跳过
  • Session 切换时清除所有追踪状态

5.2 Agent 事件(工具调用)

tool.start  → chatLog.startTool(toolCallId, toolName, args)
tool.result → chatLog.updateToolResult(toolCallId, result)

工具调用是否显示受 verbose 级别控制——verbose=off 时静默处理。

5.3 并发 Run 处理

typescript
const hasConcurrentActiveRun = (runId: string) => {
  const activeRunId = state.activeChatRunId;
  if (!activeRunId || activeRunId === runId) return false;
  return sessionRuns.has(activeRunId);
};

当检测到并发运行时,只刷新非本地发起的 run 的历史(避免重复加载自己刚发送的消息)。

![事件处理——Chat 事件与 Agent 事件路由](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/05-infographic-event-handlers-1775150684756.png)


六、命令系统 —— commands.ts + tui-command-handlers.ts

6.1 斜杠命令注册

TUI 注册了 20+ 个本地斜杠命令 + Gateway 的全部远程命令:

命令处理位置功能
/help本地显示帮助
/status本地 → Gateway RPC显示状态摘要
/model <name>本地 → Gateway RPC切换模型
/models本地打开模型选择器
/agent <id>本地切换 Agent
/agents本地打开 Agent 选择器
/session <key>本地切换会话
/sessions本地打开会话选择器
/think <level>本地 → Gateway RPC设置思考级别
/verbose <on|off>本地工具输出显示
/new / /reset本地 → Gateway RPC重置会话
/abort本地 → Gateway RPC中止运行
/settings本地打开设置面板
/exit / /quit本地退出 TUI
Gateway 命令远程动态注册,透传到 Gateway

6.2 命令自动补全

每个命令可以注册 getArgumentCompletions() 回调:

typescript
{ name: "think", getArgumentCompletions: (prefix) =>
    thinkLevels.filter(v => v.startsWith(prefix)).map(value => ({ value, label: value }))
}

当用户输入 /think h 时,TUI 自动提示 high

6.3 选择器叠层

模型/Agent/Session 选择都通过叠层(Overlay)实现:

typescript
const openModelSelector = async () => {
  const models = await client.listModels();
  const selector = createSearchableSelectList(items, 9);  // 最多显示 9 行
  openSelector(selector, async (value) => {
    await client.patchSession({ key: state.currentSessionKey, model: value });
  });
};

支持两种选择器:

  • SearchableSelectList:输入过滤(实时搜索)
  • FilterableSelectList:按类别过滤

![命令系统——斜杠命令·自动补全·选择器](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/06-infographic-command-system-1775150685882.png)


七、命令处理上下文

CommandHandlerContext 是命令处理器的依赖注入容器:

typescript
type CommandHandlerContext = {
  client: GatewayChatClient;       // Gateway 通信
  chatLog: ChatLog;                // 聊天日志组件
  tui: TUI;                       // TUI 框架
  opts: TuiOptions;               // 启动选项
  state: TuiStateAccess;          // 共享状态
  deliverDefault: boolean;        // 是否默认投递到渠道
  openOverlay / closeOverlay;     // 叠层控制
  refreshSessionInfo / loadHistory; // 数据刷新
  setSession / refreshAgents;     // 导航
  abortActive;                    // 中止运行
  noteLocalRunId / forgetLocalRunId; // 本地 run 追踪
  requestExit;                    // 退出请求
};

通过上下文注入而非全局状态,让命令处理器可以独立测试。


八、状态管理

TuiStateAccess 定义了 TUI 的共享状态:

状态类型说明
currentSessionKeystring当前会话 key
currentAgentIdstring当前 Agent ID
agentDefaultIdstring默认 Agent ID
agentsAgentSummary[]Agent 列表
activeChatRunIdstring | null活跃的 AI 运行 ID
sessionInfoSessionInfo会话信息(模型/状态/verbose 等)
toolsExpandedboolean工具输出是否展开
showThinkingboolean是否显示思考过程
connectedboolean连接状态

状态变更直接触发 tui.requestRender(),按需重绘。

![CommandHandlerContext 依赖注入与 TuiStateAccess 共享状态](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/07-infographic-context-state-1775150686665.png)


九、设计模式总结

模式应用位置效果
Context InjectionCommandHandlerContext命令处理器解耦
Stream AssemblyTuiStreamAssembler累积文本 → 显示文本
Run TrackingfinalizedRuns + sessionRuns防止重复处理 + 并发安全
Overlay StackopenOverlay/closeOverlay模态选择器
Auto-CompletegetArgumentCompletions斜杠命令智能补全
Dual Source本地命令 + Gateway 命令统一命令体验
Pruning MappruneRunMap()有界内存(200 条上限)

![TUI 设计模式总览——7 大模式](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(二十)TUI 终端交互界面/08-infographic-design-patterns-1775150687568.png)


十、推荐阅读顺序

  1. src/tui/commands.ts — 斜杠命令注册 + 解析
  2. src/tui/tui-types.ts — 核心类型定义
  3. src/tui/gateway-chat.ts — Gateway 通信客户端
  4. src/tui/tui-stream-assembler.ts — 流式文本组装
  5. src/tui/tui-event-handlers.ts — 事件处理(chat + agent)
  6. src/tui/tui-command-handlers.ts — 命令处理器
  7. src/tui/components/ — UI 组件(chatLog, selectors, statusLine)
  8. src/tui/tui.ts — 主入口(7 阶段启动流程)

十一、思考题

  1. TUI 和 Web UI 都是 Gateway 的客户端——它们的代码能否抽象出共享层? 两者的事件处理逻辑(chat delta/final/error)几乎相同。

  2. finalizedRunssessionRuns 的双 Map 设计是否过度复杂? 它解决了什么具体的并发问题?

  3. 选择器使用 Overlay 模式而非页面切换——在终端环境中哪种更合适? 考虑终端尺寸限制和用户习惯。

  4. TUI 的命令系统同时注册本地命令和 Gateway 远程命令——如果命名冲突怎么办? 当前的优先级策略是什么?

读文档、看源码、写代码,理解 AI Agent 本质 🤖