Skip to main content
BCC
s13

Ink 入门

终端 UI

React 渲染终端

~300 行代码9 个工具Ink 框架 + Box/Text 组件 + render 替换 console.log
终端 UI 和 Web UI 的心智模型是一样的——组件 + 状态 + 渲染

React is not just for browsers — it renders terminals too

Your terminal is just another render target

[ Phase 3: 终端 UI ] · 工具数: 9 · 代码量: ~150 行


前置知识

  • 需要完成: s12 [工具注册表]

你将学到

  • Ink 框架——用 React 组件渲染终端 UI
  • Box/Text 组件替代 console.log
  • Agent 输出通过回调解耦,实现 UI 与逻辑分离

前 12 课的 CLI 全靠 console.log + readline 输出。随着消息增多,你会发现:

  • 工具输出和 assistant 回复混在一起,难以区分
  • 想加个 Spinner、进度条?只能手写 ANSI 转义码;
  • readline 的输入体验极其有限——无法上下翻页、无法多行编辑。

Claude Code 的做法是引入 Ink——一个让你用 React 组件渲染终端 UI 的框架。

设计决策

为什么选 Ink 而非 blessed/terminal-kit?

Ink 的核心理念:终端和浏览器的心智模型一样——组件 + 状态 + 渲染

  • Box = HTML 的 div(Flexbox 布局)
  • Text = HTML 的 span(文字样式)
  • useState / useEffect 和 Web React 完全相同
  • 前端开发者零学习曲线

Agent 输出解耦

console.log 替换为 onOutput 回调:

// s12: 直接输出
console.log(`assistant> ${text}`);

// s13: 回调解耦
onOutput({ type: "assistant", content: text });

这使 Agent 与 UI 完全分离——同一个 Agent 可以接 Ink、接 Web、接测试。

实现要点

// cli.tsx — 一行代码把 React 渲染到终端
import { render } from "ink";
render(<App />);

// components/app.tsx — Box/Text 替代 console.log
<Box flexDirection="column">
  {lines.map(line => (
    <Box key={line.id}>
      <Text color={colorMap[line.type]}>{line.content}</Text>
    </Box>
  ))}
</Box>

Agent 的 run 方法接受回调,UI 组件通过 useState 管理消息列表:

await agent.run(input, (out) => {
  setLines(prev => [...prev, { type: out.type, content: out.content }]);
});

运行验证

cd agents/s13-ink-basics
npm install
npm run dev
# 看到 Ink 渲染的彩色终端界面
# 输入问题,观察消息以 React 组件形式渲染
# ESC 退出

对照 Claude Code

方面教学版Claude Code
渲染入口render(<App />)ink.ts 封装 render + createRoot + ThemeProvider
组件基础原生 Box / TextThemedBox / ThemedText(设计系统包装)
Agent 输出onOutput 回调React 状态驱动(useAppState)

生产版的 ink.ts 还会注入 ThemeProvider,使所有子组件都能访问主题色。

深入思考

  1. 为什么 render 是异步的? Ink 内部创建了 React reconciler 实例,需要挂载到终端 stdout。
  2. Ink 和 React DOM 的区别? 渲染目标不同——一个写 ANSI 到 stdout,一个写 DOM 到 document。但 Reconciler 调度完全一致。
  3. 回调 vs 事件 vs ObservableonOutput 回调是最简方案;生产版会用 React state + context 做更精细的更新控制。

练习

  1. 给 App 组件添加一个当前时间显示(用 useEffect + setInterval),体验 Ink 的重渲染机制。
  2. 尝试把 BoxflexDirection 改为 "row",观察布局变化。
  3. 对比 agents/s12-tool-registry/src/cli.tsagents/s13-ink-basics/src/cli.tsx,列出所有差异点。

下一课预告

s13 实现了基础渲染,但所有消息看起来都一样。下一课 s14 消息列表 将为用户消息、AI 回复、工具调用等不同类型设计独立的渲染组件。