Cline 的终端 UI 实战:基于 OpenTUI 平台 Skill 构建 TUI 的完整指南
OpenTUI 是一套运行在 Bun 之上、以 Zig 原生绑定为底层的终端用户界面(TUI)框架,提供 Core(命令式)、React(reconciler)、Solid(reconciler)三种编程模型。本文以 Cline 仓库内置的 OpenTUI 平台 Skill 文档为核心骨架,完整梳理其关键规则、三套框架的选型决策树、组件与布局能力、测试方案及常见坑点;并结合 Cline CLI 自身基于 @opentui/react 构建交互式终端界面的真实用法加以佐证,读完后可掌握从零搭建、配置、开发、测试一个 OpenTUI 应用的完整路径。
一、Skill 文档结构:如何组织一份可检索的技术知识库
OpenTUI SKILL.md 并非普通 README,而是一份为 Agent 与开发者设计的“可执行技能文档”。它的组织方式值得借鉴:
- 每个框架目录遵循 5 文件模式:
REFERENCE.md(总览与快速上手,始终先读)、api.md(运行时 API/组件/Hooks)、configuration.md(工程配置、tsconfig、打包)、patterns.md(常见模式与最佳实践)、gotchas.md(陷阱、限制、调试); - 横切概念单文件成组:Layout、Components、Keyboard、Animation、Testing 各以
REFERENCE.md为入口; - 推荐阅读顺序:先读所选框架的
REFERENCE.md,再按任务跳转——构建组件读api.md+components/<分类>.md,配工程读configuration.md,布局读 layout/REFERENCE.md,键盘输入读 keyboard/REFERENCE.md,动画读 animation/REFERENCE.md,排障读gotchas.md+ testing/REFERENCE.md。
对应的实际文件路径如下(以仓库根目录为起点):
.agents/skills/opentui/references/core/REFERENCE.md # Core 入口
.agents/skills/opentui/references/react/api.md # React 组件与 Hooks
.agents/skills/opentui/references/solid/configuration.md # Solid 工程配置
.agents/skills/opentui/references/components/inputs.md # 输入类组件
.agents/skills/opentui/references/core/gotchas.md # 核心调试指南
二、四条关键规则(Critical Rules)
SKILL.md 把以下四条规则列为所有 OpenTUI 代码必须遵守的底线,后文的坑点章节均有详细展开:
- 新项目一律用
create-tui脚手架,各框架的快速上手见对应REFERENCE.md; create-tui的选项必须放在参数之前:bunx create-tui -t react my-app有效,bunx create-tui my-app -t react无效;- 绝不直接调用
process.exit():应使用renderer.destroy()先完成终端清理(详见 core/gotchas.md); - React/Solid 中文本样式必须用嵌套标签:使用
<strong>、<span fg="red">等修饰元素,而不是 prop。
三、框架选型决策树
SKILL.md 给出的核心决策树(原样继承):
Which framework?
├─ 我要完全控制、极致性能、无框架开销 → core/(命令式 API)
├─ 我熟悉 React,想要熟悉的组件范式 → react/(React reconciler)
├─ 我要细粒度响应式、最优重渲染 → solid/(Solid reconciler)
└─ 我在基于 OpenTUI 构建库/框架 → core/(命令式 API)
3.1 Core(@opentui/core):命令式 API
据 core/REFERENCE.md,Core 是基础库,提供全部原语:Renderer(管理终端输出、输入事件、渲染循环)、Renderables(带 Yoga 布局的层级化 UI 构件)、Constructs(声明式组合封装)、FrameBuffer(自定义图形的低层 2D 渲染面)。适用场景:在 OpenTUI 之上构建框架/库、需要最小打包体积(无 React/Solid 运行时)、性能关键应用、集成既有命令式代码库;不适用于熟悉 React 模式或追求快速原型的典型应用(此时应选 @opentui/react 或 @opentui/solid)。
推荐用脚手架创建:
bunx create-tui@latest -t core my-app
cd my-app
bun run src/index.ts
注意:CLI 会替你创建 my-app 目录,它必须事先不存在;SKILL 文档还特别提示,Agent 场景必须使用 -t <template> 自主模式,绝不能用不带 -t 的交互模式(Agent 无法应答终端提问)。
手动搭建与最小示例:
mkdir my-tui && cd my-tui
bun init
bun install @opentui/core
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
// 创建盒子容器
const container = new BoxRenderable(renderer, {
id: "container",
width: 40,
height: 10,
border: true,
borderStyle: "rounded",
padding: 1,
})
// 在盒子内创建文本
const greeting = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
// 组合渲染树
container.add(greeting)
renderer.root.add(container)
Core 中 Renderables(命令式,new TextRenderable(renderer, {...}),创建时需传 renderer,通过方法直接变更)与 Constructs(声明式,Text({...}) 创建 VNode,链式调用被记录并在实例化时重放)二选一,分别对应“完全控制”与“更干净的组合”。
3.2 React(@opentui/react):JSX + Hooks
据 react/REFERENCE.md,React reconciler 提供:自定义 reconciler(组件渲染为 OpenTUI renderables)、JSX 内建元素(<text>、<box>、<input> 等)、Hooks(useKeyboard、useRenderer、useTimeline 等)、完整 React 兼容(useState、useEffect、context)。
快速上手:
bunx create-tui@latest -t react my-app
cd my-app
bun run src/index.tsx
或手动安装:bun install @opentui/react @opentui/core react,最小示例:
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { useState } from "react"
function App() {
const [count, setCount] = useState(0)
return (
<box border padding={2}>
<text>Count: {count}</text>
<box border onMouseDown={() => setCount(c => c + 1)}>
<text>Click me!</text>
</box>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
注意 JSX 元素不是 HTML 元素:<text> → TextRenderable,<box> → BoxRenderable,<input> → InputRenderable,<select> → SelectRenderable。文本内部使用修饰元素而非 prop:
<text>
<strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
<span fg="red">Colored text</span>
<br />
New line with <a href="https://example.com">link</a>
</text>
样式支持直连 prop(<box backgroundColor="blue" padding={2} border>)与 style 对象(<box style={{ backgroundColor: "blue", padding: 2, border: true }}>)两种方式。
仓库佐证:Cline CLI 正是 React reconciler 的生产用户。apps/cli/package.json 中声明了 "@opentui/core": "0.4.3"、"@opentui/react": "0.4.3"、"react": "19.2.4" 与 react-reconciler 依赖;而 apps/cli/src/tui/index.tsx 的入口写法与文档示例完全一致:import { createCliRenderer } from "@opentui/core" + import { createRoot } from "@opentui/react",随后 const renderer = await createCliRenderer({...}) 再 createRoot(renderer) 挂载组件树。仓库中还有 index.test.ts 等测试文件,对 createCliRenderer/createRoot 做了 mock 与行为断言,印证了该调用链是 CLI TUI 的核心路径。
3.3 Solid(@opentui/solid):细粒度响应式
据 solid/REFERENCE.md,Solid reconciler 提供基于信号的细粒度响应式(只有变化部分重渲染)、Portal 与 Dynamic 高级组合原语,适合性能关键、已知 SolidJS 的团队。
bunx create-tui@latest -t solid my-app
cd my-app && bun install
选项:--no-git(跳过 git init)、--no-install(跳过 bun install)。手动安装:bun install @opentui/solid @opentui/core solid-js。
import { render } from "@opentui/solid"
import { createSignal } from "solid-js"
function App() {
const [count, setCount] = createSignal(0)
return (
<box border padding={2}>
<text>Count: {count()}</text>
<box border onMouseDown={() => setCount(c => c + 1)}>
<text>Click me!</text>
</box>
</box>
)
}
render(() => <App />)
信号与副作用用 createSignal/createEffect 表达;Solid 的 JSX 内建元素中部分使用下划线命名(<tab_select>、<ascii_font>、<line_number>),而 React 对应元素用连字符(<tab-select>、<ascii-font>、<line-number>)——这是两套框架最易混淆的命名差异。
Solid 还有两个特有高级组件:
// Portal:把子节点渲染到另一个挂载节点(如覆盖层)
import { Portal } from "@opentui/solid"
<Portal mount={renderer.root}>
<box position="absolute" left={10} top={5} border>
<text>Overlay content</text>
</box>
</Portal>
// Dynamic:动态切换组件
import { Dynamic } from "@opentui/solid"
<Dynamic
component={props.multiline ? "textarea" : "input"}
placeholder="Enter text..."
/>
四、组件能力地图与命名对照
SKILL.md 的 Product Index 将组件按四类组织(入口见 components/REFERENCE.md):
| 分类 | 入口文件 | 组件 |
|---|---|---|
| 文本与展示 | components/text-display.md | text、ascii-font、styled text |
| 容器 | components/containers.md | box、scrollbox、边框 |
| 输入 | components/inputs.md | input、textarea、select、tab-select |
| 代码与 Diff | components/code-diff.md | code、line-number、diff、markdown、text-table |
三套框架的组件命名对照(节选):
| 概念 | Core(类名) | React(JSX) | Solid(JSX) |
|---|---|---|---|
| 文本 | TextRenderable |
<text> |
<text> |
| 盒子 | BoxRenderable |
<box> |
<box> |
| Tab 选择 | TabSelectRenderable |
<tab-select> |
<tab_select> |
| ASCII 字体 | ASCIIFontRenderable |
<ascii-font> |
<ascii_font> |
| 行号 | LineNumberRenderable |
<line-number> |
<line_number> |
| 表格 | TextTableRenderable |
N/A(Core 专用) | N/A(Core 专用) |
“我要展示内容/我要接收输入/我要布局/我要动画/我要处理输入/我要测试/我要排障”七棵决策树,把具体需求映射到上表中的对应参考文件,例如:可滚动内容区 → containers.md 的 scrollbox;代码语法高亮、unified/split diff、带行号与诊断 → code-diff.md;流式 Markdown → code-diff.md 的 markdown;单行/多行输入、列表选择、Tab 选择 → inputs.md。
4.1 布局:Yoga/Flexbox 模型
据 layout/REFERENCE.md,OpenTUI 布局基于 Facebook 的 Yoga 引擎,提供类 CSS Flexbox 能力,尺寸单位是字符格(columns x rows),支持百分比相对尺寸。核心容器属性:
flexDirection:row(默认,水平排列)/column/row-reverse/column-reverse;justifyContent:flex-start、flex-end、center、space-between、space-around、space-evenly;alignItems:交叉轴对齐;- 定位与尺寸:
position="relative" | "absolute"、left/top/right/bottom、width/height/min*/max*; - Flex 与间距:
flexGrow/flexShrink/flexBasis、flexWrap、gap、padding*/margin*(含paddingX/paddingY/marginX/marginY轴向简写); - 显示控制:
display="flex" | "none"、overflow="visible" | "hidden" | "scroll"、zIndex。
五、测试体系:无头渲染器 + 快照 + 交互模拟
据 testing/REFERENCE.md,OpenTUI 提供 Test Renderer(无头渲染器)、快照测试、交互测试三件套,测试运行器使用 Bun 内置 bun:test。
5.1 Core 测试:createTestRenderer
import { test, expect } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { TextRenderable } from "@opentui/core"
test("renders text", async () => {
const testSetup = await createTestRenderer({
width: 40,
height: 10,
})
const text = new TextRenderable(testSetup.renderer, {
id: "greeting",
content: "Hello, World!",
})
testSetup.renderer.root.add(text)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toContain("Hello, World!")
})
快照测试把渲染结果(如 ┌──...┐ 框线文本)作为纯文本比对;更新快照用 bun test --update-snapshots。
5.2 React 测试:testRender(test-utils 子路径)
import { test, expect } from "bun:test"
import { testRender } from "@opentui/react/test-utils"
function Greeting({ name }: { name: string }) {
return <text>Hello, {name}!</text>
}
test("Greeting renders name", async () => {
const testSetup = await testRender(
<Greeting name="World" />,
{ width: 80, height: 24 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toContain("Hello, World!")
})
testRender 会自动创建无头渲染器、设置 React Act 环境、在 destroy 时正确卸载。返回对象包含:renderer(无头实例)、renderOnce()(触发一次渲染周期)、captureCharFrame()(把当前输出捕获为文本)、resize(width, height)(调整虚拟终端尺寸)。
5.3 Solid 测试:testRender(主包导出)
与 React 的唯一关键差异:Solid 的 testRender 接收函数组件而非 JSX 元素:
import { testRender } from "@opentui/solid"
test("Greeting renders name", async () => {
const testSetup = await testRender(
() => <Greeting name="World" />,
{ width: 80, height: 24 }
)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toContain("Hello, World!")
})
5.4 交互与焦点测试
模拟按键直接通过 renderer.keyInput.emit("keypress", {...}) 派发事件(需给出 name、sequence、修饰键布尔、eventType: "press" 等完整字段),随后 renderOnce() 再断言帧内容;焦点断言则通过 input.focus() 后检查 input.isFocused()。
测试的三条铁律(Gotchas 章节):
- 异步渲染:捕获帧前必须
await testSetup.renderOnce(); - 测试隔离:每个测试后在
afterEach中testSetup.renderer.destroy(),避免资源泄漏; - 快照尺寸一致性:统一使用如
80 x 24的标准虚拟终端尺寸,保证快照稳定。
六、坑点与调试:从 core/gotchas.md 继承的实战细节
据 core/gotchas.md,以下是最值得提前知道的运行时约束:
6.1 运行时环境:Bun 而非 Node.js
OpenTUI 面向 Bun 构建,应使用 bun install / bun run / bun test,而不是 npm install / node / npx jest。应用代码也应优先使用 Bun 内置 API:Bun.serve(替代 express)、Bun.$`ls -la`(替代 execa)、bun:sqlite(替代 better-sqlite3);环境变量由 Bun 自动加载 .env,无需 dotenv。文档注明 OpenTUI 自身内部使用 node:fs 以保证更宽的兼容性,但你的应用代码仍应优先 Bun API。
6.2 不要用 process.exit(),用 renderer.destroy()
直接 process.exit() 会跳过终端清理,可能把终端留在损坏状态(备用屏幕模式、raw 输入模式等)。正确顺序是:
// 错误:终端可能残留损坏状态
if (error) {
console.error("Fatal error")
process.exit(1)
}
// 正确:先 destroy 清理,再退出
if (error) {
console.error("Fatal error")
await renderer.destroy()
process.exit(1) // 只在 destroy 之后
}
// 更好:让 renderer 自己处理退出
const renderer = await createCliRenderer({ exitOnCtrlC: true })
renderer.destroy() // 程序化退出
renderer.destroy() 会在退出前把终端恢复到原始状态。
6.3 看不到 console.log 输出
OpenTUI 会捕获 console 输出用于调试 overlay,运行期终端里看不到日志。四种解法:
- 打开 console overlay:
renderer.console.show(); - 键盘切换:监听
keypress中key.name === "f12"时renderer.console.toggle(); - 写文件:
appendFileSync("debug.log", ...); - 禁用捕获:
OTUI_USE_CONSOLE=false bun run src/index.ts。
6.4 焦点管理
输入组件只有被 focus 后才接收键盘输入:input.focus();嵌套在容器里时应聚焦具体组件而不是容器(container.focus() 是错的),可用 container.getRenderable("input")?.focus(),或在 Constructs 中用 delegate({ focus: "input" }, Box({}, Input({ id: "input" }))) 把焦点路由给内部 input。
6.5 构建要求与 Zig
只有修改原生(Zig)代码才需要 bun run build;TypeScript 改动无需构建(Bun 直接运行 TS)。Zig 需预先安装(macOS 用 brew,Linux 从官网下载)。
6.6 常见错误速查
- “Cannot read properties of undefined”:通常是 renderable 没挂进渲染树,务必
renderer.root.add(text)后再调用其方法; - 布局不更新:Yoga 布局是惰性计算的,改完布局属性后调用
renderer.requestRender()强制重算; - 文本溢出/截断:文本默认不自动换行,需显式设置
width; - 颜色不显示:颜色值必须是
#FF0000(十六进制)、"red"(CSS 颜色名)或RGBA.fromHex("#FF0000");"FF0000"(缺#)与数字0xFF0000均不支持; - 性能:批量更新减少渲染触发、避免无意义的深层嵌套、用
setDisplay("none")切换可见性而非反复add/remove节点。
6.7 键盘事件命名
KeyEvent.name 常见取值:字母数字("a"…"z"、"0"…"9")、特殊键("escape"、"enter"、"return"、"tab"、"backspace"、"delete"、方向键、"home"/"end"、"pageup"/"pagedown"、"f1"…"f12"、"space");修饰键是布尔属性(key.ctrl、key.shift、key.meta(Alt)、key.option(macOS));key.eventType 区分 "press" | "release" | "repeat"。
七、排障索引与参考资料
SKILL.md 内置的 Troubleshooting Index 给出了“症状 → 文档”的直达映射:
| 症状 | 应查文档 |
|---|---|
| 终端清理异常、崩溃 | core/gotchas.md |
| 文本样式不生效 | components/text-display.md |
| 输入焦点/快捷键问题 | keyboard/REFERENCE.md |
| 布局错位 | layout/REFERENCE.md |
| 快照测试不稳定 | testing/REFERENCE.md |
组件命名差异与文本修饰符的统一说明见 components/REFERENCE.md。
Skill 文档还登记了上游资源(OpenTUI 官方仓库及其 core 包文档、示例与社区 Awesome 列表),本文依规范不输出外部链接,需要深入时可按包名 @opentui/core、@opentui/react、@opentui/solid 检索官方资料。
八、小结:从 Skill 文档到可落地的开发流程
把上述内容串起来,一个标准的 OpenTUI 开发闭环是:
- 选型:按团队技术栈与性能诉求在 core / react / solid 间决策(参考第三节决策树);
- 创建工程:
bunx create-tui@latest -t <core|react|solid> my-app(选项在前、目录不存在、Agent 必须用自主模式); - 开发:按需求从组件四类与布局/键盘/动画参考中取对应能力;遵守四条关键规则(脚手架、选项顺序、
destroy()退出、嵌套标签样式); - 测试:
createTestRenderer/testRender+renderOnce()+captureCharFrame()做断言与快照,按键用keyInput.emit模拟,afterEach中销毁渲染器; - 排障:对照排障索引与各框架
gotchas.md,重点关注 Bun 运行时、终端清理、焦点与惰性布局这几类高频坑。
Cline 仓库自身的实践印证了这套体系的可扩展性:CLI 的交互式终端(apps/cli/src/tui/ 下的组件、hooks 与视图)完全构建在 @opentui/react 0.4.3 之上,并配套了 TUI 测试工具链(apps/cli/package.json 中的 @microsoft/tui-test、tuistory 与 test:e2e:tuistory 脚本),说明 OpenTUI 不仅适用于小型工具,也支撑着生产级 Agent 终端界面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00