s13
Ink Basics
Terminal UIReact 渲染终端
~300 lines of code9 toolsInk 框架 + 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 / Text | ThemedBox / ThemedText(设计系统包装) |
| Agent 输出 | onOutput 回调 | React 状态驱动(useAppState) |
生产版的 ink.ts 还会注入 ThemeProvider,使所有子组件都能访问主题色。
深入思考
- 为什么 render 是异步的? Ink 内部创建了 React reconciler 实例,需要挂载到终端 stdout。
- Ink 和 React DOM 的区别? 渲染目标不同——一个写 ANSI 到 stdout,一个写 DOM 到 document。但 Reconciler 调度完全一致。
- 回调 vs 事件 vs Observable:
onOutput回调是最简方案;生产版会用 React state + context 做更精细的更新控制。
练习
- 给 App 组件添加一个当前时间显示(用
useEffect+setInterval),体验 Ink 的重渲染机制。 - 尝试把
Box的flexDirection改为"row",观察布局变化。 - 对比
agents/s12-tool-registry/src/cli.ts和agents/s13-ink-basics/src/cli.tsx,列出所有差异点。
下一课预告
s13 实现了基础渲染,但所有消息看起来都一样。下一课 s14 消息列表 将为用户消息、AI 回复、工具调用等不同类型设计独立的渲染组件。