How does opencode render custom markdown with JSON code blocks?
我发现 opencode 的 session 可以用 markdown 的方式复制出去,而且这个 markdown 文件具有特殊的 code block 结构,这个结构中存在 opencode 自定义的 json 结构,opencode tui 在渲染的时候会对这个结构进行特殊的渲染,详细解释整个工作机制;然后详细解释 opencode 是如何渲染这个特殊 markdown 的,使用的是什么库
完美!现在我可以清楚地看到整个结构了。让我整理一份完整的说明文档:
##OpenCode Session Markdown 导出与渲染机制详解
我已经彻底研究了 opencode 的 session 复制/导出系统,现在为你详细解释整个工作机制。
###一、核心工作机制概览
OpenCode 的 Session markdown 导出是一个三层架构:
Session Data (JSON)
↓
↓ [转换层 - formatTranscript]
↓
Markdown 文件 (特殊结构)
↓
↓ [渲染层 - @opentui markdown parser]
↓
TUI 终端显示
###二、Session Markdown 的特殊结构
####2.1 整体布局
导出的 markdown 文件按以下结构组织(见 ./packages/opencode/src/cli/cmd/tui/util/transcript.ts):
# [Session Title]
**Session ID:** [session-id]
**Created:** [timestamp]
**Updated:** [timestamp]
---
## User
[user message text...]
---
## Assistant ([Agent] · [Model] · [Duration])
[assistant message text...]
---
####2.2 Tool 调用的特殊 Code Block 结构
这是 opencode 最核心的创新——用特殊的 markdown code block 来表示 tool 执行:
Tool: [tool-name]
Input:
{
"filePath": "path/to/file",
"...": "..."
}
Output:
[tool execution result...]
Error:
[error message if any]
关键特点:
- 使用无语言标识的 code block (
```) - 第一行是
Tool: [tool-name]- 这是 opencode 自定义的标记 - 包含嵌套的 code block:
**Input:**下是```json格式的输入参数**Output:**下是原始格式的输出**Error:**下是错误信息(仅在失败时)
###三、Markdown 转换层(formatTranscript)
源文件:./packages/opencode/src/cli/cmd/tui/util/transcript.ts
export function formatTranscript(
session: SessionInfo,
messages: MessageWithParts[],
options: TranscriptOptions,
): string
转换流程:
- Header 生成:session 基本信息
- 消息遍历:对每条消息调用
formatMessage() - Part 处理:根据 part 类型调用
formatPart()
formatPart 的处理逻辑:
if (part.type === "text") {
// 普通文本,直接输出
return `${part.text}\n\n`
}
if (part.type === "reasoning") {
// 思考过程(Claude 的 extended thinking)
return `_Thinking:_\n\n${part.text}\n\n`
}
if (part.type === "tool") {
// Tool 调用 - 生成特殊的 code block 结构
let result = `\`\`\`\nTool: ${part.tool}\n`
// 添加 Input(JSON 格式)
if (options.toolDetails && part.state.input) {
result += `\n**Input:**\n\`\`\`json\n`
result += JSON.stringify(part.state.input, null, 2)
result += `\n\`\`\``
}
// 添加 Output
if (part.state.status === "completed" && part.state.output) {
result += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
}
// 添加 Error
if (part.state.status === "error" && part.state.error) {
result += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
}
result += `\n\`\`\`\n\n`
return result
}
###四、TUI 导出与复制(Clipboard)
源文件:./packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
####4.1 导出路径
Copy to Clipboard:
const transcript = formatTranscript(sessionData, messages, options)
await Clipboard.copy(transcript) // 直接复制到剪贴板
Export to File:
const transcript = formatTranscript(sessionData, messages, options)
const filepath = path.join(exportDir, filename)
await Bun.write(filepath, transcript) // 保存为 .md 文件
####4.2 Clipboard 实现
源文件:./packages/opencode/src/cli/cmd/tui/util/clipboard.ts
支持多平台的复制:
export async function copy(text: string): Promise<void> {
writeOsc52(text) // OSC 52 escape sequence (用于 SSH)
await getCopyMethod()(text) // 平台特定方法
}
平台实现:
- macOS:
osascript(AppleScript) - Linux Wayland:
wl-copy - Linux X11:
xclip或xsel - Windows:PowerShell
- Fallback:
clipboardynpm 库
###五、Markdown 渲染层(@OpenTUI)
OpenCode 使用 @opentui/core 库来渲染 markdown。这是最关键的部分。
####5.1 使用的库
- marked:Markdown 解析库(支持 GFM 扩展)
- Tree-Sitter:语法高亮解析器
- @opentui/core:OpenTUI 核心渲染引擎
源代码位置:
- OpenTUI Markdown 渲染:
anomalyco/opentui仓库的packages/core/src/renderables/Markdown.ts - OpenTUI Code 渲染:
packages/core/src/renderables/Code.ts
####5.2 Markdown 解析过程
// OpenTUI 的 markdown-parser.ts
export function parseMarkdownIncremental(
newContent: string,
prevState: ParseState | null,
trailingUnstable: number = 2,
): ParseState {
// 使用 marked 库解析
const tokens = Lexer.lex(newContent, { gfm: true })
return { content: newContent, tokens }
}
关键特点:
- 增量解析(incremental parsing):支持流式内容更新
- GFM 支持:GitHub Flavored Markdown,包括表格、删除线等
####5.3 Token 处理
OpenTUI 的 Markdown 渲染器将 markdown 转换为 Renderable 对象:
Markdown String
↓
Marked Tokens (AST)
↓
Renderable Objects (TextRenderable, CodeRenderable, BoxRenderable)
↓
Terminal Display
关键方法(Markdown.ts):
private createDefaultRenderable(
token: MarkedToken,
index: number
): Renderable | null {
// Code block 处理
if (token.type === "code") {
return this.createCodeRenderable(token, id, marginBottom)
}
// Table 处理
if (token.type === "table") {
return this.createTableRenderable(token, id, marginBottom)
}
// 其他文本内容
const chunks = this.renderTokenToChunks(token)
return this.createTextRenderable(chunks, id, marginBottom)
}
private createCodeRenderable(
token: Tokens.Code,
id: string,
marginBottom: number = 0
): Renderable {
return new CodeRenderable(this.ctx, {
id,
content: token.text, // Code block 的文本内容
filetype: token.lang || undefined, // 语言类型(json, python 等)
syntaxStyle: this._syntaxStyle,
conceal: this._conceal,
treeSitterClient: this._treeSitterClient,
width: "100%",
marginBottom,
})
}
####5.4 Code Block 的特殊处理
当遇到 opencode 的 Tool code block 时:
Tool: read
**OpenTUI 的处理**:
1. `token.lang` 为 `undefined`(因为 ``` 后面没有语言标识)
2. `token.text` 为整个 code block 的内容
3. OpenTUI 会将其识别为**无语言的 code block**
**重点**:opencode 目前**没有自定义渲染器**。Tool code block 被作为普通的 code block 渲染,显示为:
- 第一行 "Tool: [name]" 作为普通文本
- 嵌套的 code block 被 markdown 解析器识别为额外的内容
#### 5.5 语法高亮实现
Code block 的语法高亮由 **Tree-Sitter** 提供:
```typescript
export class CodeRenderable extends TextBufferRenderable {
private _treeSitterClient: TreeSitterClient
// 根据 filetype 调用相应的 Tree-Sitter 语法解析器
private async highlightContent(): Promise<void> {
const highlights = await this._treeSitterClient.getHighlights(
this._content,
this._filetype
)
// 应用样式
}
}
支持的语言(来自 parsers-config.ts):
- Python, Rust, Go, C++, C#, Bash, C, Java, JavaScript, TypeScript, JSON, SQL, YAML 等
对于 json code block(Tool Input),Tree-Sitter 会应用 JSON 高亮。
###六、OpenCode TUI 中的 Markdown 渲染
源文件:./packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
import { addDefaultParsers } from "@opentui/core"
import parsers from "../../../../../../parsers-config.ts"
// 初始化 Tree-Sitter 语法解析器
addDefaultParsers(parsers.parsers)
// 渲染文本部分
export function TextPart(props: { part: TextPart }) {
return (
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
{/* 使用 @opentui/solid 的 markdown 组件 */}
<markdown
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
{/* 降级方案:使用 code 组件作为 fallback */}
<code
filetype="markdown"
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match>
)
}
关键元素:
<markdown>组件:OpenTUI Solid 的 JSX 组件,对应MarkdownRenderable类<code>组件:OpenTUI Solid 的 JSX 组件,对应CodeRenderable类- 有一个 实验性的 markdown 标志(
OPENCODE_EXPERIMENTAL_MARKDOWN),用来切换新旧渲染方式
###七、工作流程总结
┌─────────────────────────────────────────────────────────────┐
│ OpenCode Session │
│ - Messages (User/Assistant with Parts) │
│ - Parts: Text, Tool, Reasoning │
└─────────────────────────────────────────────────────────────┘
↓ (用户按导出键)
┌─────────────────────────────────────────────────────────────┐
│ DialogExportOptions 显示选项 │
│ - Include Thinking (推理过程) │
│ - Include Tool Details (Tool 输入/输出) │
│ - Include Assistant Metadata (模型/时长) │
│ - Save to File or Open in Editor │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ formatTranscript() 转换层 │
│ - 遍历每条 message 和 part │
│ - Part Type 判断: │
│ * text → 直接输出文本 │
│ * reasoning → "_Thinking:_\n\n[text]" │
│ * tool → "```\nTool: [name]\n[Input/Output]```" │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 生成的 Markdown 文件 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ # My Session │ │
│ │ **Session ID:** abc123 │ │
│ │ --- │ │
│ │ ## User │ │
│ │ Read the file config.json │ │
│ │ --- │ │
│ │ ## Assistant (Claude · claude-3.5 · 2.3s) │ │
│ │ ``` │ │
│ │ Tool: read │ │
│ │ │ │
│ │ **Input:** │ │
│ │ ```json │ │
│ │ { "filePath": "config.json" } │ │
│ │ ``` │ │
│ │ **Output:** │ │
│ │ ``` │ │
│ │ { "host": "localhost", ... } │ │
│ │ ``` │ │
│ │ ``` │ │
│ │ The file contains... │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Clipboard.copy() / Bun.write() │
│ - 复制到剪贴板(支持 OSC 52 用于 SSH) │
│ - 或保存到文件 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ @opentui Markdown 渲染层 │
│ │
│ MarkdownRenderable: │
│ parseMarkdownIncremental() │
│ ↓ │
│ Marked Lexer → MarkedToken[] │
│ ↓ │
│ Token Type 判断: │
│ * heading → TextRenderable │
│ * code → CodeRenderable (含 Tree-Sitter 高亮) │
│ * table → BoxRenderable (表格布局) │
│ * paragraph → TextRenderable │
│ │
│ CodeRenderable: │
│ - 识别 filetype (如 "json") │
│ - 调用 Tree-Sitter 客户端获取高亮信息 │
│ - 应用 SyntaxStyle 样式 │
│ - 渲染到 TextBuffer │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ TUI 终端显示 │
│ 带有语法高亮的格式化 markdown 渲染 │
└─────────────────────────────────────────────────────────────┘
###八、关键代码文件位置
| 功能 | 文件位置 |
|---|---|
| Markdown 转换 | packages/opencode/src/cli/cmd/tui/util/transcript.ts |
| TUI Session 视图 | packages/opencode/src/cli/cmd/tui/routes/session/index.tsx |
| 导出对话框 | packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx |
| 剪贴板操作 | packages/opencode/src/cli/cmd/tui/util/clipboard.ts |
| CLI 导出命令 | packages/opencode/src/cli/cmd/export.ts |
| 语法配置 | packages/opencode/parsers-config.ts |
| OpenTUI Markdown 渲染 | anomalyco/opentui: packages/core/src/renderables/Markdown.ts |
| OpenTUI Code 渲染 | anomalyco/opentui: packages/core/src/renderables/Code.ts |
| Markdown 解析 | anomalyco/opentui: packages/core/src/renderables/markdown-parser.ts |
###九、使用的核心库
| 库 | 版本 | 用途 |
|---|---|---|
@opentui/core | 0.1.75 | TUI 渲染引擎 |
@opentui/solid | 0.1.75 | Solid.js 绑定 |
marked | - | Markdown 解析 |
tree-sitter-* | 各版本 | 语法高亮 (Python, Rust, Go 等) |
web-tree-sitter | 0.25.10 | Tree-Sitter WebAssembly 客户端 |
clipboardy | 4.0.0 | Fallback 剪贴板操作 |
###十、重要特点
- 增量渲染:Markdown 支持流式更新,适合 AI 生成内容的实时显示
- 多平台支持:剪贴板操作支持 macOS、Linux、Windows,以及 SSH 环境(OSC 52)
- 灵活的导出选项:用户可选择包含的内容(思考、Tool 详情、元数据)
- 高效的语法高亮:通过 Tree-Sitter WASM 提供多语言支持
- Fallback 机制:有降级方案处理不支持的 markdown 特性
这就是 opencode 的完整 session markdown 导出和渲染机制!
END OF POST