# TUI 优化:渲染性能与可扩展性 > 详细设计文档 — 提升渲染性能,支持更多格式,增强主题可配置性,探索远期方向。 ## 1. 问题分析 ### 1.1 Markdown 解析器现状 当前使用自定义正则逐行解析器(`packages/cli/src/ui/utils/MarkdownDisplay.tsx`,461 行): ```typescript // MarkdownDisplayInternal 核心循环 const lines = text.split(/\r?\n/); const headerRegex = /^ *(#{1,4}) +(.*)/; const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/; const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/; const olItemRegex = /^([ \t]*)(\d+)\. +(.*)/; const hrRegex = /^ *([-*_] *){3,} *$/; const tableRowRegex = /^\s*\|(.+)\|\s*$/; const tableSeparatorRegex = /^(?=.*\|)\s*\|?\s*(:?-+:?)\s*(\|\s*(:?-+:?)\s*)*\|?\s*$/; // 在循环中逐行用这 7 个正则匹配,无解析结果缓存 for (let i = 0; i < lines.length; i++) { // headerRegex.exec(line) // codeFenceRegex.exec(line) // ulItemRegex.exec(line) // ... 逐个正则尝试匹配当前行 } ``` **问题**: 1. **无解析缓存**:每次 React re-render 都对完整文本重新解析。流式输出时,每新增一个 token 就重新解析所有已累积文本 2. **功能受限**:不支持 GFM 任务列表、脚注、嵌套格式、定义列表等 3. **正则脆弱性**:边界情况处理不完整,如嵌套代码块、复杂列表、未闭合流式 Markdown 等 4. **性能线性退化**:文本越长,每帧解析耗时线性增长 ### 1.2 代码高亮现状 `packages/cli/src/ui/utils/CodeColorizer.tsx`(224 行): ```typescript import { common, createLowlight } from 'lowlight'; const lowlightInstance = createLowlight(common); // 启动时加载 ~40 种语法 ``` **问题**: 1. **急切加载**:`import { common }` 在模块级别加载约 40 种语言语法到内存,增加启动时间和内存占用 2. **无高亮缓存**:每次渲染相同代码块都重新调用 `lowlight.highlight()` 3. **`highlightAuto()` 昂贵**:未指定语言时的自动检测需遍历所有已注册语法 ### 1.3 表格渲染现状 `packages/cli/src/ui/utils/TableRenderer.tsx`(540 行): **源码校准**: - 当前实现已经使用 `wrap-ansi`、`strip-ansi` 和 string-width 缓存处理 ANSI/CJK 宽度 - 已有基本表格、CJK、ANSI、宽度边界和 vertical fallback 的回归测试 - 因此表格不应作为 Phase 1 的主要重构目标;除非有 qwen-code 当前版本可复现缺陷,否则以补 fixture 和保护现有能力为主 **仍需验证的风险**: - 与新 Markdown token/cache 层集成后,表格 token 到现有 `TableRenderer` 的输入是否保持一致 - 极窄宽度、混合 ANSI + CJK + emoji 场景是否仍能触发 vertical fallback - marked 迁移后对齐语法、转义 pipe、代码 span 中 pipe 的处理是否与当前渲染兼容 ### 1.4 主题系统现状 `packages/cli/src/ui/themes/theme-manager.ts`: ```typescript // 大多数主题使用 hex 颜色 export const QwenDark: Theme = { name: 'QwenDark', colors: { Background: '#0b0e14', Foreground: '#bfbdb6', AccentBlue: '#39BAE6', // ... }, }; ``` **问题**: 1. **hex 颜色硬编码**:绕过终端调色板,破坏透明背景终端 2. **无终端能力检测**:不区分 truecolor/256 色/16 色终端 3. **仅 ANSI/ANSILight 使用 16 色**:但非默认主题 ### 1.5 缺失的渲染能力 | 能力 | 现状 | 用户需求 | | ------------------ | ------------------ | ----------------- | | LaTeX 数学公式 | 不支持 | claude-code#21433 | | 终端超链接 (OSC 8) | URL 渲染为纯文本 | 点击跳转 | | 虚拟滚动 | 无,长会话性能退化 | 长会话场景 | | 图表/图像 | 不支持 | 远期探索 | ### 1.6 Gemini CLI / Claude Code 调研结论 外部源码调研说明,渲染层的机会不能只看“Markdown 支持哪些语法”,而要同时看 parser、streaming、highlight、表格和长会话容器: | 维度 | Gemini CLI | Claude Code | 对 qwen-code 的含义 | | --- | --- | --- | --- | | Markdown parser | 仍是自定义正则解析器 | `marked` + token cache + plain-text fast path | parser 架构升级应主要参考 Claude,而不是把 Gemini 当 parser 终局 | | 流式 Markdown | `findLastSafeSplitPoint()` + Static 提升 | `StreamingMarkdown` 稳定前缀 / 不稳定尾部 | 现有“安全分割点”方向正确,但应升级成稳定块模型 | | 代码高亮 | 同步 `lowlight(common)` | Suspense + fallback + 宽度感知渲染 | qwen-code 应坚持“同步基线 + 异步增强” | | 表格 | 已有成熟 ANSI/CJK 宽度处理 | `MarkdownTable` 单独组件化 | 表格不是首要重构目标,但应成为 parser 迁移的兼容边界 | | 长会话 | `ScrollableList` / `VirtualizedList` | `ScrollBox` / `useVirtualScroll` / `VirtualMessageList` | 虚拟滚动必须进入正式路线图,且要处理动态高度与 resize | 因此,本设计文档后续的重点不应只是“换 parser”,而是把 parser、streaming、高亮、虚拟滚动作为一组相互制约的问题来处理。 ## 2. 解决方案 ### 2.1 [P0] Markdown token/block 缓存 **目标**:消除流式输出时的重复解析开销。 **关键约束**:不能缓存 `React.ReactNode[]`。`MarkdownDisplay` 的最终渲染受 `isPending`、`availableTerminalHeight`、`contentWidth`、`textColor`、主题、代码行号设置等 props/settings 影响;按文本 hash 缓存 ReactNode 会导致 resize、主题切换、pending 高度裁剪和行号开关后复用错误结果。 **方案**:实现 block 级别的 LRU 缓存,但缓存对象是 token/block 元数据,而不是 ReactNode。 **设计**: ```typescript // 新增缓存层 const PARSE_CACHE_MAX = 500; const parseCache = new LRUCache(PARSE_CACHE_MAX); interface ParsedMarkdownBlock { type: 'paragraph' | 'heading' | 'code' | 'table' | 'list' | 'hr'; raw: string; attrs: Record; children?: ParsedMarkdownBlock[]; } function parseMarkdownBlocks(text: string): ParsedMarkdownBlock[] { const cacheKey = hashContent(text); const cached = parseCache.get(cacheKey); if (cached) return cached; // ... 现有解析逻辑 ... const blocks = doParseBlocks(text); parseCache.set(cacheKey, blocks); return blocks; } function renderMarkdownBlocks( blocks: ParsedMarkdownBlock[], props: MarkdownDisplayProps, ): React.ReactNode[] { // 根据当前 width/theme/pending/height/settings 渲染,不能跨 props 复用 } ``` **流式优化**:利用现有的 `findLastSafeSplitPoint()` 实现增量解析。 ```` 全文: "# Title\n\nParagraph 1\n\nParagraph 2\n\n```code block..." ├──── 已完成块 ────┤├── 已完成块 ──┤├── 当前块 ──┤ 缓存命中(不重解析) 缓存命中 重新解析(仅此块) ```` **缓存 key**: - parse cache:`hash(rawBlock)` + parser version - render 辅助缓存(如纯文本 wrap 结果):必须额外包含 `contentWidth`、theme identity、`isPending`、height constraint、settings 版本 - 不把完整原始长字符串作为 key 保存,避免内存放大 **影响范围**:`packages/cli/src/ui/utils/MarkdownDisplay.tsx` **预期收益**:缓存命中时解析耗时显著下降。对于 1000 行的流式输出,每帧仅需解析最后一个不完整块(通常 < 50 行),而非全部 1000 行。 **参考**:Claude Code 使用模块级 LRU 缓存(500 条目),key 为内容 hash,避免保留完整字符串引用;qwen-code 应采用 token/block 级缓存以适配 Ink props 驱动渲染。 ### 2.2 [P0] 代码高亮优化 **关键约束**:当前 `colorizeCode()` 是同步函数,直接返回 ReactNode;因此不能在 render 路径中直接 `await ensureLanguage()`。语法库懒加载必须配合 Suspense、预热队列或“当前帧纯文本 fallback,下一帧高亮增强”的状态模型,否则会破坏 Ink 同步渲染路径。 **方案 A:同步基线 + 异步预热** ```typescript // 当前(急切加载) import { common, createLowlight } from 'lowlight'; const lowlightInstance = createLowlight(common); // 优化方向:保留小型同步基础语法,稀有语法异步预热 import { createLowlight } from 'lowlight'; const lowlightInstance = createLowlight(BASELINE_GRAMMARS); const GRAMMAR_LOADERS: Record Promise> = { javascript: () => import('highlight.js/lib/languages/javascript'), typescript: () => import('highlight.js/lib/languages/typescript'), python: () => import('highlight.js/lib/languages/python'), // ... 常用语言 }; function requestLanguageWarmup(lang: string): void { if (lowlightInstance.registered(lang)) return; const loader = GRAMMAR_LOADERS[lang]; if (!loader) return; void loader().then((grammar) => { lowlightInstance.register(lang, grammar.default); emitHighlightCacheInvalidated(lang); }); } ``` **渲染策略**: - 已注册语言:同步高亮 - 未注册但可加载语言:本帧纯文本/简化高亮,同时触发 warmup;下一次 render 使用高亮 - 未指定语言:限制 `highlightAuto()` 的输入大小和语言集合,超大代码块直接纯文本,避免遍历所有 grammar - pending streaming 代码块:默认不做昂贵高亮,完成后再高亮 **方案 B:高亮结果缓存** ```typescript const highlightCache = new LRUCache(200); function cachedHighlight(input: HighlightInput): HighlightResult { const key = [ input.language ?? 'auto', input.themeId, input.showLineNumbers, input.contentWidth, input.availableTerminalHeight ?? 'none', hashContent(input.code), ].join(':'); const cached = highlightCache.get(key); if (cached) return cached; const result = highlightSynchronously(input); highlightCache.set(key, result); return result; } ``` **缓存 key 必须包含**: - code hash、language/auto mode、registered grammar version - theme identity / color palette - `showLineNumbers` - `contentWidth` - `availableTerminalHeight` 或裁剪后的 line range - pending vs completed 状态(pending 可直接禁用缓存或单独缓存) **影响范围**:`packages/cli/src/ui/utils/CodeColorizer.tsx` **预期收益**: - 同步基线 + 异步预热:减少启动时模块加载量,降低内存占用,同时不破坏同步 render - 缓存:对已完成代码块的重复渲染耗时降至 O(1) ### 2.3 [P1] 切换到 marked 解析器 **动机**:当前自定义正则解析器的功能和鲁棒性已接近上限。`marked` 是 Claude Code 的选择,提供成熟的 block/inline lexer API,可作为 v2 渲染器候选。但迁移必须先定义安全策略和流式不完整语法策略,不能只替换 parser。 **架构设计**: ``` 输入文本 ├─ 快速路径检测: /[#*`|[\->_~]|\n\n|^\d+\. / (无 MD 语法 → 纯文本渲染) ├─ marked.lexer(text) → Token[] (AST) └─ 自定义 Renderer: Token[] → React.ReactNode[] ├─ heading → ├─ code → (复用现有组件) ├─ table → (复用现有组件) ├─ list → (复用现有组件) ├─ paragraph → (复用现有组件) ├─ blockquote → └─ ... 其他 token 类型 ``` **流式优化**: ```typescript // 仅对最后一个不完整块调用 marked.lexer() const blocks = splitAtBlockBoundaries(streamingText); const cachedBlocks = blocks.slice(0, -1).map((b) => getCachedTokens(b)); const lastBlockTokens = marked.lexer(blocks[blocks.length - 1]); return [...cachedBlocks.flat(), ...lastBlockTokens]; ``` **新增 GFM 能力**: | 能力 | marked 支持 | 当前解析器 | |---|---|---| | 标准表格 | 是,需映射到现有 `TableRenderer` | 已有自定义实现 | | 任务列表 `- [x]` | 是,需自定义 Ink renderer | 否 | | 脚注 `[^1]` | 需通过扩展/插件策略验证,不作为首批默认承诺 | 否 | | 删除线 `~~text~~` | 是 | 是 | | 自动链接 | 是 | 部分 | | HTML 内联 | parser 可识别;qwen-code 需默认转义或忽略,不能直接渲染 HTML | 仅 `` | | 嵌套格式 | 更完整,但需 fixture 验证 Ink renderer 行为 | 受限 | **必须先定的策略**: - HTML policy:默认忽略或转义 HTML;不允许把 marked 输出的 HTML 当作安全内容直接渲染 - Extension policy:脚注、定义列表等非首批能力需单独开关和 fixture,不在 v2 默认承诺里混入 - Streaming policy:未闭合代码块、表格、列表时,最后一个 block 允许降级为纯文本或 v1 行解析,避免 token 结构抖动 - Compatibility policy:现有 `InlineMarkdownRenderer` 的 `[text](url)` 输出形态、表格 fallback、代码块裁剪行为必须有 fixture 对照 **迁移策略**: 1. 添加 `marked` 依赖 2. 创建 `MarkdownDisplayV2.tsx`,使用 marked lexer + 自定义 renderer 3. 默认关闭,通过设置项 `ui.markdownRenderer: 'v1' | 'v2'` 和环境变量双重切换 4. 编写 Markdown fixture 测试集,对比两个渲染器输出,重点覆盖 streaming partial blocks 5. 内部 dogfood 后渐进切换默认值到 v2,保留 v1 作为回退 6. 稳定两个小版本后再评估移除 v1 **来自 Claude Code 的额外校准**: - `marked` 迁移的真正收益不只是语法支持,而是 token cache、plain-text fast path、流式稳定前缀可以一起落地 - 表格应继续组件化渲染,避免为了 parser 迁移把表格退回到纯文本路径 - 如果只替换 parser 而不补 cache / streaming policy,收益会明显低于预期 **影响范围**: - 新增:`packages/cli/src/ui/utils/MarkdownDisplayV2.tsx` - 修改:`packages/cli/src/ui/utils/MarkdownDisplay.tsx`(特性开关) - 修改:`package.json`(添加 marked 依赖) **风险点**: - marked 的 token 结构与当前组件的 props 接口需要适配 - 流式 markdown 中的不完整语法可能导致 marked 产生不同的 token 结构 - marked 本身不负责 HTML sanitize,必须由 qwen-code renderer 定义安全策略 - 添加依赖会影响 bundle 体积,需要纳入 `processUptimeAtT0Ms` 和 bundle analyzer - 缓解:保留 v1 作为回退,充分测试后再切换默认值 ### 2.4 [P1] 主题系统 — ANSI 16 色默认 + 终端能力检测 **目标**:默认使用 ANSI 16 色主题,确保兼容所有终端(包括透明背景、自定义配色方案)。 **终端能力检测逻辑**: ```typescript // packages/cli/src/ui/themes/theme-manager.ts function detectColorCapability(): 'truecolor' | '256' | '16' | 'none' { if (process.env.NO_COLOR !== undefined) return 'none'; if (process.env.FORCE_COLOR === '3') return 'truecolor'; const colorterm = process.env.COLORTERM?.toLowerCase(); if (colorterm === 'truecolor' || colorterm === '24bit') return 'truecolor'; const term = process.env.TERM || ''; if (term.includes('256color')) return '256'; return '16'; // 保守默认 } function getDefaultTheme(): Theme { const capability = detectColorCapability(); switch (capability) { case 'none': return NoColorTheme; case 'truecolor': return QwenDark; // hex 颜色主题 default: return ANSI; // 16 色主题,尊重终端调色板 } } ``` **明暗主题自动检测**(进阶): ```typescript // 通过 OSC 11 查询终端背景色 function queryTerminalBackground(): Promise<'light' | 'dark' | 'unknown'> { return new Promise((resolve) => { const timeout = setTimeout(() => resolve('unknown'), 1000); process.stdout.write('\x1b]11;?\x07'); // OSC 11 查询 // 解析响应判断明暗... }); } ``` OSC 11 查询会向终端请求背景色响应,可能与用户输入流、tmux/SSH 组合和非交互输出产生副作用。该能力只作为 opt-in 进阶功能,不作为默认启动路径的一部分;默认策略应优先基于 `NO_COLOR`、`FORCE_COLOR`、`COLORTERM`、`TERM` 和用户显式主题设置。 **影响范围**: - `packages/cli/src/ui/themes/theme-manager.ts` — 添加能力检测,修改默认主题选择 - `packages/cli/src/ui/themes/semantic-tokens.ts` — 确保 ANSI 主题的语义 token 完整 **向后兼容**: - 已在 settings 中显式设置主题的用户不受影响 - 仅影响未设置主题的新用户或重置用户 - 所有 hex 颜色主题仍可通过设置选择 ### 2.5 [P2] OSC 8 终端超链接 **目标**:将 URL 和 Markdown 链接渲染为可点击的终端超链接。 **OSC 8 协议**: ``` ESC ] 8 ; params ; uri ST ← 开始超链接 link text ← 显示文本 ESC ] 8 ; ; ST ← 结束超链接 // 示例 \x1b]8;;https://example.com\x07Click here\x1b]8;;\x07 ``` **支持的终端**:iTerm2, kitty, WezTerm, Windows Terminal, Hyper, foot, Contour 等。不支持或禁用 OSC 8 的场景应保持当前纯文本 fallback。 **实现**: ```typescript // 新增工具函数 function wrapHyperlink(url: string, text: string): string { return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`; } ``` 在 `InlineMarkdownRenderer.tsx` 中集成: - `[text](url)` → OSC 8 包裹的可点击链接 - 自动检测的 URL → OSC 8 包裹 - 文件路径 → `file://` URL 包裹(如工具输出中的文件路径) **安全与兼容**: - URL 必须过滤控制字符和 OSC 终止符,避免注入额外 escape sequence - 仅允许明确协议白名单(如 `http:`, `https:`, `file:`),其他协议按纯文本渲染 - 不支持 OSC 8 或禁用超链接时,保持当前 `text (url)` 的可复制 fallback - 在 screen reader 模式下默认使用纯文本 fallback **影响范围**: - `packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx` — 链接渲染修改 - 新增:超链接工具函数模块 ### 2.6 [P2] 消息历史虚拟滚动(Phase 3) **现状**:所有历史消息通过 `` 追加到终端 scrollback,长会话会产生大量渲染元素。 **调研结论先行**:这不是一个“列表 slice 一下”的小优化。Gemini CLI 的 `VirtualizedList` 和 Claude Code 的 `useVirtualScroll` 都表明,真正可用的消息虚拟滚动至少要处理: - 动态高度消息 - 贴底行为(sticky bottom) - resize 后高度缓存失效 - overscan - 搜索/跳转/定位 - 复制模式 / 选择模式 - 渲染中间态不出现 blank spacer **方案设计**: ``` ┌─────────────────────────────┐ │ Overscan (上方 2 条) │ ← 预渲染但不可见 ├─────────────────────────────┤ │ │ │ 可见区域 (终端高度) │ ← 当前渲染 │ │ ├─────────────────────────────┤ │ Overscan (下方 2 条) │ ← 预渲染但不可见 └─────────────────────────────┘ │ 未渲染消息 (跳过) │ ← 按需加载 ``` **关键挑战**: - Ink 的 `` 是追加模式,无法移除已渲染内容 - 需要切换到 alternate screen 模式或自行管理终端输出 - 每条消息的高度需要预计算或缓存 **应补充的工程约束**: 1. **滚动输入不要每 tick 都走 React setState** Claude 的 `ScrollBox` 直接操作 DOM scrollTop,`useVirtualScroll` 只在量化后的 snapshot 变化时触发 React commit。qwen-code 如果让 wheel/scroll 直接驱动高频 state 更新,后续所有虚拟化收益都会被抵消。 2. **高度缓存不能在 resize 时简单清空** Claude 采用“按列宽比例缩放旧高度 + 冻结旧 range 两帧”的策略,Gemini 也用 `ResizeObserver` 和实测高度维护 offsets。qwen-code 需要把 resize 视为一等场景,而不是异常路径。 3. **要为 sticky bottom 与 copy/search mode 预留语义** Gemini 的 `VirtualizedList` 暴露 `isStickingToBottom`、`stableScrollback`、`copyModeEnabled`;Claude 也把 sticky signal 视为核心状态。qwen-code 若未来要支持 transcript 搜索、selection 或 copy mode,不应把虚拟滚动写成只服务普通聊天输出的最小实现。 4. **初期只建议在 fullscreen / alternate buffer 路径启用** Gemini 的经验表明,这类滚动容器最适合全屏或 buffer 模式;main-screen 路径继续用 `Static` + pending 区域更保守。 **参考**:Claude Code 的 `` 和 `useVirtualScroll` 形成了完整的滚动/贴底/overscan/resize 体系;Gemini CLI 的 `ScrollableList` / `VirtualizedList` 则证明这一层可以先在 alternate/fullscreen 路径落地。 **建议**:先评估 Phase 1-2 的优化效果,若长会话性能仍是痛点再实施。 ### 2.7 [P3] LaTeX/数学公式渲染 **场景**:代码辅助场景中,模型输出可能包含数学公式(如算法分析、信号处理等)。 **方案层次**: **Level 1:Unicode 数学符号替换(可行性高)** ``` $x^2 + y^2 = z^2$ → x² + y² = z² $\alpha + \beta$ → α + β $\frac{1}{2}$ → ½ $\sum_{i=1}^{n}$ → Σᵢ₌₁ⁿ ``` 使用 `tex-to-unicode` 库或自建映射表,覆盖常见数学符号。 **Level 2:块级公式语法高亮(可行性中)** ``` $$ E = mc^2 $$ ``` 识别 `$$...$$` 块,使用语法高亮渲染 LaTeX 源码(类似代码块但标注为 `latex`)。 **Level 3:完整 KaTeX 渲染到终端(可行性低)** - 需要实现 KaTeX 的 AST 到终端渲染的转换 - 终端能力有限(无下标对齐、无分数线等) - 可能需要图像协议(Sixel/Kitty image protocol) **建议**:Phase 3 实现 Level 1 + Level 2,Level 3 作为远期探索。 ### 2.8 [远期] Web 渲染探索 **动机**:终端能力终究有限,复杂的富文本渲染(图表、公式、交互式表格)在 Web 环境中更自然。 **探索方向**: 1. **混合架构**:CLI 进程处理输入和工具执行,通过 WebSocket 将富文本内容推送到本地浏览器伴侣界面 2. **Electron/Tauri 封装**:将终端嵌入 Web 壳中(类似 VS Code 终端),获得 CSS/SVG/Canvas 完整能力 3. **Kitty Image Protocol**:在支持的终端中内联显示图像(图表截图、公式渲染图等) **收益**: - 完整 CSS 样式 - SVG 图表 - MathJax/KaTeX 数学公式 - 交互式表格(排序、筛选) - 图像内联显示 **风险**: - 增加系统复杂度和依赖 - 偏离纯 CLI 工具的定位 - 需要额外的安装步骤 **建议**:仅作为概念验证(POC),不纳入正式路线图。 ## 3. 竞品参考与路线校准 ### 3.1 Gemini CLI:滚动和渲染模式先行 Gemini CLI 在 parser 架构上并没有比 qwen-code 更先进,但它在长会话和渲染模式上已经形成了可借鉴的组合: | 能力 | 实现方式 | | --- | --- | | 长会话容器 | `ScrollableList` / `VirtualizedList` | | item 级稳定渲染 | `StaticRender` | | 高度测量 | `ResizeObserver` | | 贴底行为 | `scrollAnchor` + `isStickingToBottom` | | scrollback / copy mode | `stableScrollback` / `copyModeEnabled` | **关键差异**:Gemini 说明“渲染扩展”不仅是 parser 选择,还包括长会话容器和消息呈现模式。 ### 3.2 Claude Code:parser、streaming 与虚拟滚动一体化 | 能力 | 实现方式 | | --- | --- | | Markdown 解析 | `marked` 库 + LRU token 缓存(500 条) | | 快速路径 | 正则检测无 MD 语法 → 跳过 `marked.lexer()` | | 流式优化 | `StreamingMarkdown` 稳定前缀,仅重解析最后一个块 | | 代码高亮 | `` 包裹的可选 CLI 语法高亮 | | 表格 | React 组件 `` | | 超链接 | OSC 8 终端超链接 | | 长会话 | `ScrollBox` + `useVirtualScroll` + `VirtualMessageList` | **关键差异**:Claude Code 将 parser、streaming、高亮、虚拟滚动视为同一套渲染架构的一部分,因此能在长会话中同时保持功能完整和性能稳定。 ## 4. 实施优先级与里程碑 | 优先级 | 方案 | 周次 | 风险 | 预期收益 | | ------ | ----------------------------------- | ----- | ------ | ------------------------- | | P0 | Markdown token/block 缓存 | 3 | 低 | 解析耗时显著下降 | | P0 | 代码高亮缓存 + 同步基线/异步预热 | 3 | 中 | 重复渲染消除,降低大块代码成本 | | P1 | ANSI 16 色默认 + 能力检测 | 4 | 中 | 修复透明终端兼容性 | | P1 | 切换到 marked 解析器 | 7-8 | 中 | GFM 基础能力增强 | | P1 | streaming stable prefix / suffix | 7-8 | 中 | 流式重解析成本显著下降 | | P2 | OSC 8 终端超链接 | 9-10 | 低 | URL 可点击 | | P2 | fullscreen / alternate 路径虚拟滚动 | 13-15 | 高 | 长会话性能 | | P3 | LaTeX 数学公式 | 15-16 | 中 | 数学内容渲染 | | 远期 | Web 渲染探索 | TBD | 探索性 | 富文本能力 | ## 5. 验证方案 除本节外,实施前还应对照 `06-implementation-rollout-checklist.md` 中“渲染与扩展验收清单”的退出标准。 ### 5.1 渲染性能基准 ```typescript // 测试用例 const benchmarks = [ { name: '短文本', content: '一段简短的回复', expectedParseMs: '<1' }, { name: '500行 Markdown', content: generateMd(500), expectedParseMs: '<5' }, { name: '代码块×10', content: generateCodeBlocks(10), expectedParseMs: '<10', }, { name: '大表格 (20×5)', content: generateTable(20, 5), expectedParseMs: '<5', }, { name: '流式 1000 token', content: simulateStream(1000), expectedRerenders: '<20', }, ]; ``` ### 5.2 格式兼容性测试 Markdown fixture 测试集,验证所有支持的格式正确渲染: - 标题(H1-H4) - 代码块(带语言标注 + 无语言 + 嵌套) - 表格(基本 + 对齐 + CJK 内容 + 宽字符) - 表格回归(ANSI + CJK + emoji、极窄宽度、vertical fallback、代码 span 中 pipe) - 列表(有序 + 无序 + 嵌套 + 混合) - 内联格式(加粗 + 斜体 + 代码 + 链接 + 删除线) - 分割线 - 引用块 - streaming partial blocks(未闭合代码块、未闭合表格、未闭合列表) - stable prefix / unstable suffix 切换场景 - HTML 输入(默认转义/忽略策略) - resize 后高度缓存与虚拟滚动 range 稳定性 ### 5.3 主题兼容性 | 终端 | ANSI 16 色 | 256 色 | Truecolor | 透明背景 | | ---------------- | ------------ | ------ | --------- | ------------- | | iTerm2 | 正确 | 正确 | 正确 | ANSI 模式正确 | | Terminal.app | 正确 | 正确 | N/A | ANSI 模式正确 | | kitty | 正确 | 正确 | 正确 | ANSI 模式正确 | | WezTerm | 正确 | 正确 | 正确 | ANSI 模式正确 | | Windows Terminal | 正确 | 正确 | 正确 | ANSI 模式正确 | | NO_COLOR 环境 | NoColor 主题 | — | — | — |