Cline CLI 中的 OpenTUI 代码与 Diff 组件实战:语法高亮、行内诊断与统一/分栏 Diff 渲染
本文以 Cline 仓库内 OpenTUI 技能参考文档 .agents/skills/opentui/references/components/code-diff.md 为主体,系统讲解 OpenTUI 提供的 code、line-number、diff、markdown、text-table 五类"代码展示"组件:它们的属性参数、回调签名(onHighlight / onChunks)、链接自动识别、行高亮与诊断(diagnostics)API、分栏 diff 的同步滚动机制,以及 Solid/React 命名差异、Tree-sitter 依赖等常见陷阱。Cline 的 CLI(TUI 界面)正是构建在 @opentui/react(0.4.3,见 apps/cli/package.json)之上,读完本文你可以掌握在终端 TUI 中渲染带语法高亮代码块、诊断标注和 diff 视图的完整技术方案,并能直接对照 Cline 的 TUI 源码验证这些组件的真实用法。
1. 组件全景:Code & Diff 家族
这份参考文档在 OpenTUI 技能索引(SKILL.md)中被归类为"Code & Diff"组件家族,承担四类展示需求:
| 需求 | 组件 |
|---|---|
| 带语法高亮的代码块 | code |
| 行号 + 高亮 + 诊断信息 | line-number |
| 统一/分栏 diff 查看器 | diff |
| 流式 Markdown 渲染 | markdown |
| 带边框/换行/可选中文本的数据表格 | text-table(TextTableRenderable) |
三者底层共享同一套 Tree-sitter 语法高亮管线,这也是后文"陷阱"章节强调必须安装对应 grammar 的原因。Cline CLI 的 TUI 中,工具输出面板就是通过 <code> 组件配合主题化的 syntaxStyle 来渲染执行结果的,例如 tool-output.tsx 中:
<code
content={fullText}
filetype="bash"
syntaxStyle={getSyntaxStyle(props.theme)}
selectable
/>
注意这里 Cline 的实际代码使用的是 content + filetype 属性组合,并传入由主题推导出的 syntaxStyle;而参考文档中给出的是 code + language 的写法。两者分别对应组件在不同 API 版本/绑定层的属性命名,阅读文档示例时以你所使用的 @opentui/react 版本的类型定义为准即可。
2. Code 组件:语法高亮代码块
2.1 基础用法(Core / React / Solid 三种形态)
OpenTUI 的组件都可以用三种方式创建:Core 的命令式 Renderable、React 调和器、Solid 调和器。文档给出的基础示例:
// React
<code
code={`function hello() {
console.log("Hello, World!");
}`}
language="typescript"
/>
// Solid
<code
code={sourceCode}
language="javascript"
/>
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
})
要点:
- 必须指定
language才会启用高亮。不带 language 的<code code={text} />只按纯文本渲染;language="typescript"时才会走 Tree-sitter 高亮(见第 8 节陷阱)。 - 高亮基于 Tree-sitter,文档列出的常见支持语言包括:
typescript、javascript、python、rust、go、json、html、css、markdown、bash/shell。
2.2 样式控制
通过内联样式属性调整背景与行号显示:
<code
code={sourceCode}
language="typescript"
backgroundColor="#1a1a2e"
showLineNumbers
/>
在 Cline 的 TUI 中,颜色不写死而来自主题:getSyntaxStyle(theme) 会依据当前主题返回一套语法 token 配色,chat-entry.tsx 与 tool-output.tsx 中的 syntaxStyle={getSyntaxStyle(props.theme)} 都是这一模式——这比文档示例中硬编码 backgroundColor="#1a1a2e" 更符合生产级 TUI 的做法。
2.3 onHighlight 回调:拦截并修改语法高亮
onHighlight 允许你在渲染前对 Tree-sitter 产出的高亮区间做增删改:
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
onHighlight: (highlights, context) => {
// 追加自定义高亮区间
highlights.push([10, 20, "custom.error", {}])
return highlights
},
})
// React/Solid
<code
code={sourceCode}
language="typescript"
onHighlight={(highlights, context) => {
// context: { content, filetype, syntaxStyle }
// 修改后返回 highlights 数组
return highlights.filter(h => h[2] !== "comment")
}}
/>
回调签名(文档原文约定):
highlights: SimpleHighlight[]—— 元素为[start, end, scope, metadata]四元组;context: { content, filetype, syntaxStyle }—— 高亮上下文;- 返回修改后的数组;返回
undefined表示沿用原始高亮。 - 支持异步回调,便于先拉取额外高亮数据(例如远端 LSP 诊断)再渲染。
典型用途:过滤掉 comment 类 scope(示例所示)、注入自定义 error 区间、或把外部 LSP 诊断映射为高亮。
2.4 onChunks 回调:高亮完成后的分块后处理
onChunks 在 onHighlight 之后执行,接收的是"已完全解析"的文本 chunk 序列,适合做渲染层面的变换:
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
onChunks: (chunks, context) => {
// 变换 chunks(例如链接检测)
return chunks
},
})
// React/Solid
<code
code={sourceCode}
language="typescript"
onChunks={(chunks, context) => {
// context: { content, filetype, syntaxStyle, highlights }
return chunks
}}
/>
注意两个回调的 context 差异:onChunks 的 context 比 onHighlight 多带一个 highlights 字段。
2.5 detectLinks:URL 自动识别工具
@opentui/core 导出的 detectLinks 可在代码文本中自动识别 URL 并写入 chunk.link,使链接在终端中可点击:
import { detectLinks } from "@opentui/core"
<code
code={sourceCode}
language="typescript"
onChunks={(chunks, context) => detectLinks(chunks, context)}
/>
其工作机制是检查 Tree-sitter 产出的 URL token,为匹配到的 chunk 设置 chunk.link;支持异步用法。这是 onChunks 回调最典型的落地场景。
3. TextTable 组件:带边框、换行与选择能力的表格
TextTableRenderable(Core)用于渲染数据表格,支持内/外边框、单元格换行、文本选择(含按列选择)。
3.1 基础用法
// Core
import { TextTableRenderable, type TextTableContent } from "@opentui/core"
const content: TextTableContent = [
[[ { text: "Name" } ], [ { text: "Age" } ], [ { text: "Role" } ]],
[[ { text: "Alice" } ], [ { text: "30" } ], [ { text: "Engineer" } ]],
[[ { text: "Bob" } ], [ { text: "25" } ], [ { text: "Designer" } ]],
]
const table = new TextTableRenderable(renderer, {
id: "table",
content,
wrapMode: "word", // "none" | "char" | "word"
columnWidthMode: "content", // "content" | "fill"
cellPadding: 0,
border: true,
outerBorder: true,
borderStyle: "single", // single | double | rounded | bold
selectable: true, // 允许文本选择
columnFitter: "balanced", // "proportional" | "balanced"
})
3.2 选项总表
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
content |
TextTableContent |
- | 二维单元格内容数组 |
wrapMode |
"none" | "char" | "word" |
"none" |
单元格内文本换行策略 |
columnWidthMode |
"content" | "fill" |
"content" |
列宽策略(按内容 / 填满) |
cellPadding |
number |
0 |
单元格内边距 |
border |
boolean |
true |
是否显示内边框 |
outerBorder |
boolean |
true |
是否显示外边框 |
borderStyle |
string |
"single" |
边框样式 |
borderColor |
string | RGBA |
- | 边框颜色 |
selectable |
boolean |
false |
是否允许文本选择 |
columnFitter |
"proportional" | "balanced" |
"proportional" |
列宽分配算法 |
3.3 单元格内容结构与选择 API
每个单元格是"带样式文本 chunk 的数组",整体结构为 行 → 单元格 → chunk 的三层嵌套:
type TextTableCellContent = { text: string; fg?: RGBA; bg?: RGBA }[]
type TextTableContent = TextTableCellContent[][] // rows -> cells -> chunks
选择能力相关的两个查询方法:
table.getSelectedText() // 获取当前选中文本
table.hasSelection() // 判断是否存在选择
并且支持按列选择(columnar selection):在单列内垂直拖拽只会选中该列的内容——这对宽表格复制某一列数据非常实用。
4. Line Number 组件:行号、高亮与诊断
line-number(Solid 中写作 line_number)在代码展示之上叠加了行号、指定行高亮与诊断标注三类能力。
4.1 基础用法
// React
<line-number
code={sourceCode}
language="typescript"
/>
// Solid(注意是下划线命名)
<line_number
code={sourceCode}
language="typescript"
/>
// Core
const codeView = new LineNumberRenderable(renderer, {
id: "code-view",
code: sourceCode,
language: "typescript",
})
4.2 行号选项
// React
<line-number
code={sourceCode}
language="typescript"
startLine={1} // 起始行号(从 N 开始编号)
showLineNumbers={true} // 是否显示行号
/>
// Solid 同形
<line_number
code={sourceCode}
language="typescript"
startLine={1}
showLineNumbers={true}
/>
startLine 在处理"文件片段"时很有用:当你只展示文件的第 100 行之后内容时,可以传 startLine={100} 使行号与真实文件位置对齐。
4.3 行高亮与诊断
行高亮:通过 highlightedLines 数组标记若干行:
// React
<line-number
code={sourceCode}
language="typescript"
highlightedLines={[5, 10, 15]}
/>
诊断信息:diagnostics 数组把错误/警告标注到指定行:
<line-number
code={sourceCode}
language="typescript"
diagnostics={[
{ line: 3, severity: "error", message: "Unexpected token" },
{ line: 7, severity: "warning", message: "Unused variable" },
{ line: 12, severity: "info", message: "Consider using const" },
]}
/>
严重级别与指示色一一对应:
error— 红色指示器warning— 黄色指示器info— 蓝色指示器hint— 灰色指示器
4.4 Diff 行高亮(added/removed)
line-number 也可以直接标记"新增行/删除行":
<line-number
code={sourceCode}
language="typescript"
addedLines={[5, 6, 7]} // 绿色背景
removedLines={[10, 11]} // 红色背景
/>
这相当于"未做完整 diff 计算时"的轻量标记方式——若你已经知道哪些行是改动行(例如来自 LSP 或补丁解析),可以直接着色而无需走 diff 组件。
5. Diff 组件:统一/分栏 Diff 查看器
diff 组件提供 unified(统一)与 split(左右分栏)两种视图,且带语法高亮。
5.1 基础用法
// React
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
/>
// Core
const diffView = new DiffRenderable(renderer, {
id: "diff",
oldCode: originalCode,
newCode: modifiedCode,
language: "typescript",
})
5.2 显示模式
// 统一 diff(默认)
<diff oldCode={old} newCode={new} mode="unified" />
// 分栏/并排 diff
<diff oldCode={old} newCode={new} mode="split" />
Core 构造参数中对应的是 view: "split"(文档示例中 diff 属性则接受已算好的 unifiedDiff 字符串)。
5.3 分栏视图的同步滚动
split 视图下,左右两个面板可以开启同步滚动:
// React/Solid
<diff
oldCode={old}
newCode={new}
mode="split"
syncScroll // 滚动一侧,另一侧同步
/>
// Core
const diffView = new DiffRenderable(renderer, {
id: "diff",
diff: unifiedDiff,
view: "split",
syncScroll: true,
})
// 运行时切换
diffView.syncScroll = true
diffView.syncScroll = false
syncScroll 是普通属性赋值即可切换的运行时开关,适合在 TUI 中做成快捷键行为。
5.4 选项与样式
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
mode="unified"
showLineNumbers
context={3} // 变更上下文的行数
/>
// 行背景配色
<diff
oldCode={old}
newCode={new}
addedLineColor="#2d4f2d" // 新增行背景
removedLineColor="#4f2d2d" // 删除行背景
unchangedLineColor="transparent"
/>
context 控制每个变更块前后保留多少未变更行,语义与 git diff -U3 的 context 参数一致,默认值可按需调小以节省终端垂直空间。
5.5 行高亮 API(Core 层)
DiffRenderable 提供一组程序化行着色 API,可覆盖样式属性设定的颜色:
// 设置单行颜色
diffView.setLineColor(5, "#2d4f2d")
diffView.setLineColor(5, { gutter: "#333", content: "#2d4f2d" })
// 清除单行颜色
diffView.clearLineColor(5)
// 批量设置多行
diffView.setLineColors(new Map([
[1, "#2d4f2d"],
[2, "#4f2d2d"],
]))
// 区间高亮
diffView.highlightLines(10, 20, "#2d4f2d")
diffView.clearHighlightLines(10, 20)
// 清除全部行颜色
diffView.clearAllLineColors()
LineNumberRenderable 同样支持程序化高亮:
lineNumberView.highlightLines(5, 10, "#2d4f2d")
lineNumberView.clearHighlightLines(5, 10)
setLineColor 的第二参数既可以是颜色字符串,也可以是 { gutter, content } 对象分别控制行号槽与内容区背景——在做"当前审阅行"指示时很有用。
6. Markdown 组件:流式 Markdown 渲染
6.1 基础用法
// React
<markdown
content={markdownText}
syntaxStyle={mySyntaxStyle}
/>
// Core
import { MarkdownRenderable } from "@opentui/core"
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "# Hello\n\nThis is **markdown**.",
syntaxStyle: mySyntaxStyle,
})
6.2 选项
<markdown
content={markdownText}
syntaxStyle={syntaxStyle}
treeSitterClient={client} // 可选:自定义 tree-sitter 客户端
conceal={true} // 隐藏 markdown 语法符号
streaming={true} // 流式模式,面向增量更新的优化
tableOptions={{ // 定制 markdown 表格渲染
widthMode: "full", // "content" | "full"
wrapMode: "word", // "none" | "char" | "word"
cellPadding: 0,
borders: true,
outerBorder: true,
borderStyle: "single",
borderColor: "#555",
selectable: true, // 表格默认可选
}}
/>
Cline CLI 的聊天消息列表正是这一组件的典型使用方:chat-entry.tsx 中在流式消息渲染时传入 syntaxStyle={getSyntaxStyle(theme, mode)},源码注释里还专门提到某个 prop 会让 MarkdownRenderable 从块级别整体重建(见 chat-entry.tsx#L662 附近的注释),说明其性能路径与"逐块增量重建"密切相关——这正是 streaming 模式存在的意义。
6.3 自定义节点渲染
renderNode 钩子允许接管任意 AST 节点的渲染:返回自定义 renderable 即可替换默认输出,返回 null 则回退到默认渲染:
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "# Custom Heading",
syntaxStyle,
renderNode: (node, ctx, defaultRender) => {
if (node.type === "heading") {
return new TextRenderable(ctx, {
content: `>> ${node.content} <<`,
})
}
return null // 使用默认渲染
},
})
6.4 流式模式:LLM 输出的实时渲染
对 LLM token 流这类"边到边渲染"场景,开启 streaming={true}:
const [content, setContent] = useState("")
useEffect(() => {
llmStream.on("token", (token) => {
setContent(c => c + token)
})
}, [])
<markdown
content={content}
syntaxStyle={syntaxStyle}
streaming={true} // 针对增量更新做了优化
/>
这一模式是 Cline CLI 聊天面板的核心交互:Agent 每吐出一个 token,消息内容就地增长,无需整页重排。
7. 组合实战:四类典型场景
文档给出了四个可直接套用的组合范例,这里保留完整代码以便复用。
7.1 代码编辑器(textarea + 行号)
function CodeEditor() {
const [code, setCode] = useState(`function hello() {
console.log("Hello!");
}`)
return (
<box flexDirection="column" height="100%">
<box height={1}>
<text>editor.ts</text>
</box>
<textarea
value={code}
onChange={setCode}
language="typescript"
showLineNumbers
flexGrow={1}
focused
/>
</box>
)
}
7.2 代码评审(分栏 diff)
function CodeReview({ oldCode, newCode }) {
return (
<box flexDirection="column" height="100%">
<box height={1} backgroundColor="#333">
<text>Changes in src/utils.ts</text>
</box>
<diff
oldCode={oldCode}
newCode={newCode}
language="typescript"
mode="split"
showLineNumbers
/>
</box>
)
}
7.3 语法高亮预览(markdown 提取代码块)
function MarkdownPreview({ content }) {
// 从 markdown 中提取代码块
const codeBlocks = extractCodeBlocks(content)
return (
<scrollbox height={20}>
{codeBlocks.map((block, i) => (
<box key={i} marginBottom={1}>
<code
code={block.code}
language={block.language}
/>
</box>
))}
</scrollbox>
)
}
7.4 错误展示(diagnostics + 行高亮)
function ErrorView({ errors, code }) {
const diagnostics = errors.map(err => ({
line: err.line,
severity: "error",
message: err.message,
}))
return (
<line-number
code={code}
language="typescript"
diagnostics={diagnostics}
highlightedLines={errors.map(e => e.line)}
/>
)
}
这个模式与 LSP 集成路径完全吻合:诊断事件 → 映射为 diagnostics 数组 + highlightedLines → 组件着色,无需手写任何字符画。
8. 常见陷阱(Gotchas)
8.1 Solid 使用下划线命名
同一组件在 React 与 Solid 中的标签命名不一致,跨框架移植时最容易踩:
// React
<line-number />
// Solid
<line_number />
8.2 不做高亮就必须不传 language
不传 language 时组件按纯文本渲染,不会报错,只是没有高亮:
// 无高亮(纯文本)
<code code={text} />
// 有 Tree-sitter 高亮
<code code={text} language="typescript" />
8.3 大文件处理策略
对超大文件,文档建议:分页或虚拟滚动、只加载可见部分、用 scrollbox 包裹:
<scrollbox height={30}>
<line-number
code={largeFile}
language="typescript"
/>
</scrollbox>
8.4 Tree-sitter 依赖排查
语法高亮依赖 Tree-sitter grammar。当高亮不生效时按顺序排查:
- 确认该语言是否受支持;
- 确认对应 grammar 已安装;
- 若使用自定义 worker 路径,检查环境变量
OTUI_TREE_SITTER_WORKER_PATH。
9. 对照 Cline 仓库的落地验证
将文档 API 与 Cline 仓库实际代码对照,可以印证两条主线:
- 属性命名随版本演进:Cline 当前锁定的
@opentui/react0.4.3(apps/cli/package.json)中,<code>使用的是content+filetype属性(如 tool-output.tsx#L85-L90 中filetype="bash"),而本文档示例使用code+language。二者语义等价,引用文档示例时应以你所装版本的 TS 类型为准,避免属性名混用。 - 样式来自主题而非硬编码:Cline TUI 统一通过
getSyntaxStyle(theme)生成语法配色并传给code/markdown(tool-output.tsx#L88、chat-entry.tsx#L502),从而让代码高亮随终端主题明暗切换。文档示例中backgroundColor="#1a1a2e"这类硬编码写法更适合原型演示。 - 流式 Markdown 是聊天面板的骨架:chat-entry.tsx 中的消息渲染围绕
MarkdownRenderable的块级重建成本做了针对性注释与优化(见 L662 附近源码注释),与本文第 6.4 节的streaming模式相互印证。
10. 小结
这份参考文档给出了 OpenTUI "代码与 Diff" 组件家族的完整 API 面:code(onHighlight / onChunks / detectLinks 三级拦截点)、text-table(选项表 + 按列选择)、line-number(诊断与 added/removed 行标记)、diff(unified/split + syncScroll + 行着色 API)、markdown(streaming + renderNode + tableOptions)。Cline CLI 的 TUI 源码(tool-output.tsx、chat-entry.tsx)则展示了这些组件在"Agent 输出面板"这类真实场景中的接线方式:主题化 syntaxStyle、filetype 自动识别、流式内容更新。按文档 API 实现、按仓库源码校准属性名,是复用这套组件最稳妥的路径。
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 StartedRust0623
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