首页
/ Cline 的终端 UI 实战:基于 OpenTUI 平台 Skill 构建 TUI 的完整指南

Cline 的终端 UI 实战:基于 OpenTUI 平台 Skill 构建 TUI 的完整指南

2026-09-06 11:40:55作者:晏闻田Solitary

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 代码必须遵守的底线,后文的坑点章节均有详细展开:

  1. 新项目一律用 create-tui 脚手架,各框架的快速上手见对应 REFERENCE.md
  2. create-tui 的选项必须放在参数之前bunx create-tui -t react my-app 有效,bunx create-tui my-app -t react 无效;
  3. 绝不直接调用 process.exit():应使用 renderer.destroy() 先完成终端清理(详见 core/gotchas.md);
  4. 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(useKeyboarduseRendereruseTimeline 等)、完整 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),支持百分比相对尺寸。核心容器属性:

  • flexDirectionrow(默认,水平排列)/column/row-reverse/column-reverse
  • justifyContentflex-startflex-endcenterspace-betweenspace-aroundspace-evenly
  • alignItems:交叉轴对齐;
  • 定位与尺寸:position="relative" | "absolute"left/top/right/bottomwidth/height/min*/max*
  • Flex 与间距:flexGrow/flexShrink/flexBasisflexWrapgappadding*/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", {...}) 派发事件(需给出 namesequence、修饰键布尔、eventType: "press" 等完整字段),随后 renderOnce() 再断言帧内容;焦点断言则通过 input.focus() 后检查 input.isFocused()

测试的三条铁律(Gotchas 章节):

  1. 异步渲染:捕获帧前必须 await testSetup.renderOnce()
  2. 测试隔离:每个测试后在 afterEachtestSetup.renderer.destroy(),避免资源泄漏;
  3. 快照尺寸一致性:统一使用如 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,运行期终端里看不到日志。四种解法:

  1. 打开 console overlay:renderer.console.show()
  2. 键盘切换:监听 keypresskey.name === "f12"renderer.console.toggle()
  3. 写文件:appendFileSync("debug.log", ...)
  4. 禁用捕获: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.ctrlkey.shiftkey.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 开发闭环是:

  1. 选型:按团队技术栈与性能诉求在 core / react / solid 间决策(参考第三节决策树);
  2. 创建工程bunx create-tui@latest -t <core|react|solid> my-app(选项在前、目录不存在、Agent 必须用自主模式);
  3. 开发:按需求从组件四类与布局/键盘/动画参考中取对应能力;遵守四条关键规则(脚手架、选项顺序、destroy() 退出、嵌套标签样式);
  4. 测试createTestRenderer / testRender + renderOnce() + captureCharFrame() 做断言与快照,按键用 keyInput.emit 模拟,afterEach 中销毁渲染器;
  5. 排障:对照排障索引与各框架 gotchas.md,重点关注 Bun 运行时、终端清理、焦点与惰性布局这几类高频坑。

Cline 仓库自身的实践印证了这套体系的可扩展性:CLI 的交互式终端(apps/cli/src/tui/ 下的组件、hooks 与视图)完全构建在 @opentui/react 0.4.3 之上,并配套了 TUI 测试工具链(apps/cli/package.json 中的 @microsoft/tui-testtuistorytest:e2e:tuistory 脚本),说明 OpenTUI 不仅适用于小型工具,也支撑着生产级 Agent 终端界面。

登录后查看全文
热门项目推荐
相关项目推荐