How does opencode render custom markdown with JSON code blocks?

HEAD
f607353fix(app): close review pane
This post might have stale content, as HEAD is 707 commits ahead.
Avatar of WhoKnowInfinity
Infinity
posted

我发现 opencode 的 session 可以用 markdown 的方式复制出去,而且这个 markdown 文件具有特殊的 code block 结构,这个结构中存在 opencode 自定义的 json 结构,opencode tui 在渲染的时候会对这个结构进行特殊的渲染,详细解释整个工作机制;然后详细解释 opencode 是如何渲染这个特殊 markdown 的,使用的是什么库

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

完美!现在我可以清楚地看到整个结构了。让我整理一份完整的说明文档:

##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

转换流程

  1. Header 生成:session 基本信息
  2. 消息遍历:对每条消息调用 formatMessage()
  3. 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)  // 平台特定方法
}

平台实现

  • macOSosascript (AppleScript)
  • Linux Waylandwl-copy
  • Linux X11xclipxsel
  • Windows:PowerShell
  • Fallbackclipboardy npm 库

###五、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/core0.1.75TUI 渲染引擎
@opentui/solid0.1.75Solid.js 绑定
marked-Markdown 解析
tree-sitter-*各版本语法高亮 (Python, Rust, Go 等)
web-tree-sitter0.25.10Tree-Sitter WebAssembly 客户端
clipboardy4.0.0Fallback 剪贴板操作

###十、重要特点

  1. 增量渲染:Markdown 支持流式更新,适合 AI 生成内容的实时显示
  2. 多平台支持:剪贴板操作支持 macOS、Linux、Windows,以及 SSH 环境(OSC 52)
  3. 灵活的导出选项:用户可选择包含的内容(思考、Tool 详情、元数据)
  4. 高效的语法高亮:通过 Tree-Sitter WASM 提供多语言支持
  5. Fallback 机制:有降级方案处理不支持的 markdown 特性

这就是 opencode 的完整 session markdown 导出和渲染机制!


END OF POST

How does opencode render custom markdown with JSON code blocks? — anomalyco/opencode