首页
/ DeepSeek Harness 渲染决策复盘:TUI 通用工具卡片为什么改用 Markdown 主题渲染

DeepSeek Harness 渲染决策复盘:TUI 通用工具卡片为什么改用 Markdown 主题渲染

2026-09-04 12:26:21作者:丁柯新Fawn

本决策记录来自 DeepSeek Harness 仓库的 .agents/notes 历史档案,讲述的是 TUI(终端界面)中"通用工具卡片"(generic card)结果内容的 Markdown 渲染问题:工具展示器(presenter)会在通用卡片里写入 Markdown——包括用于后台任务确认和执行错误的围栏 console 代码块——而旧的纯文本渲染会把围栏标记原样暴露出来,导致工具卡片与同一条 transcript(对话文本记录)中的助手消息、用户消息视觉风格不一致。读完本篇,你能理解该决策的取舍逻辑(先渲染、后截断)、三个被否决的替代方案及其原因,以及当前仓库中仍保留的渲染意图(render intent)契约与头尾截断算法如何承接这一设计。

需要注意一个时效前提:该决策记录的状态为 implemented(2026-07-23 落地,2026-08-04 归档),但仓库后续在 TUI 包移除决策 中删除了 packages/ui/tui。因此本文描述的是已归档的历史设计决策,其中"共享 Markdown 主题"的具体行为是决策记录所固定的事实;而渲染意图联合类型、通用/终端/diff 卡片契约、头尾行数限制等中性机制,在删除 TUI 之后仍然存在于当前代码中。

问题:围栏标记裸露在终端上

决策原文(2026-07-23-tui-generic-card-markdown.md中文版本)对问题的陈述是:工具展示器可以把 Markdown 写入通用卡片内容,其中典型来源是围栏 ```console 输出,用于后台任务确认(acknowledgement)和执行错误场景。这类内容如果按纯文本渲染:

  • 围栏标记(``` 行)会作为可见文本暴露给用户;
  • 工具卡片与同一 transcript 中的助手内容、用户内容显示不一致——后者本来就是 Markdown 渲染的,而工具卡片却退化成原始文本。

要理解"通用卡片"在这个体系里的位置,可以对照当前仓库的呈现契约 packages/core/tools/src/presentation.tsToolResultView 是一个以 card 字段打标签的联合类型,UI 侧按 card 字段做 switch 分发:

// packages/core/tools/src/presentation.ts#L140
export type ToolResultView =
  | GenericResultView
  | TerminalResultView
  | DiffResultView
  | SearchResultView
  | ReadResultView
  | WebResultView

其中 GenericResultView 就是"默认完成卡片",结构如下(content 承载 UI 面向的结果内容,省略则渲染原始结果文本):

// packages/core/tools/src/presentation.ts#L146-L155
export interface GenericResultView {
  card: 'generic'
  /** Replacement title for the completed call. Omit to keep the pending-state title. */
  title?: string
  /**
   * UI-facing result content (harness ContentBlocks), reformatted from
   * the model-facing result. Omit to let the UI render the raw result content.
   */
  content?: ContentBlock[]
}

也就是说,任何一个没有专属卡片能力的工具(或 UI 未注册该工具专属视图时)都会回落到通用卡片;而通用卡片的 content 恰恰是最可能包含展示器撰写的 Markdown(含围栏代码块)的通道。这正是问题发生的位置。

决策:先 Markdown 渲染,再应用头尾行数限制

决策的核心内容分三层:

1. 通用卡片的"结果内容"走共享 Markdown 主题

TUI 先用共享的 Markdown 主题渲染通用卡片的结果内容(result content),再应用卡片自身的头尾行数限制(head-and-tail line limit)。共享主题的三条具体行为由决策记录固定:

  • 隐藏围栏语法——``` 分隔行不再作为可见文本出现;
  • 保留可选的语言标签——如 ```console 中的 console 仍然显示,作为代码块的语言/来源标识;
  • 围栏正文按代码配色——块内文本使用代码样式而非正文样式着色。

这三条共同保证了:后台任务确认和执行错误里的 ```console 块,在终端上的呈现方式与同一条 transcript 中助手消息里的代码块一致。

2. 渲染先于截断

"Rendering precedes truncation" 是这个决策里最容易被忽略、但影响最远的顺序约束:

  • 收起状态(collapsed)卡片的行数统计截断边界描述的是可见的终端行(rendered terminal rows),而不是 Markdown 源文本行(source rows)。
  • 如果反过来先按源文本行截断,一个 ```console 块可能被从中间劈开(留下没有配对的围栏),且"卡片显示的行数"与"卡片实际占用的行数"不一致。

截断算法本身在仓库中是纯函数,当前实现在 packages/client/ui-primitives/src/head-tail-cap.ts

// packages/client/ui-primitives/src/head-tail-cap.ts#L23-L27
export function headTailCap(total: number, maxLines: number, expanded: boolean): HeadTailCap {
  const hidden = total - maxLines
  const headLines = Math.ceil(maxLines / 2)
  return { hidden, capped: hidden > 0 && !expanded, headLines, tailLines: maxLines - headLines }
}

语义上:hidden 为超出上限的隐藏行数(<= 0 表示未隐藏),headLines = ceil(maxLines / 2)tailLines 为头部取走后剩余的配额,expanded 为真时完全解除上限。注意它的参数 total 是"行数"——"渲染先于截断"原则要求调用方传入的是渲染后的可见行数,而不是 Markdown 源文本的行数。这一职责划分(纯算术函数只算指标,调用方自己切分行)也符合该模块"调用方用自己的 headLines/tailLines 切片、以便在之上叠加各自关注点"的设计注释。

3. 终端卡片与 diff 卡片保持专门的纯文本渲染器

决策明确划定了边界:只有通用卡片的结果内容走 Markdown 主题。终端卡片(terminal card)和 diff 卡片保留各自的专门纯文本渲染器,原因是终端输出和 diff 有专用格式,且可能包含必须按字面显示的 Markdown 标点(例如 shell 输出里的 #*-[bracket] 若被当作标题、列表、链接语法解释,语义就会被破坏)。

与此配套的一条边界同样重要:通用卡片的原始输入(rawInput)仍按字面显示。从源码契约看(presentation.ts),rawInput 是"在详情/展开视图中显示的显著输入",字符串原样渲染、对象渲染为 pretty JSON。决策记录给出的理由是:rawInput 代表的是工具参数,而不是展示器撰写的行文(presenter-authored prose)——参数值(路径、命令、查询串)被当 Markdown 解释反而有害。

当前仓库仍能看到"终端意图、通用回落"这条契约链。TerminalCallView 的注释(presentation.ts#L77-L83)写明:有能力的 UI 渲染终端卡片,无能力的 UI 回落到 body 为围栏命令输出的通用卡片TerminalResultView 的注释(presentation.ts#L157-L162)则补充:无能力 UI 会得到一个由 BRIDGE 从 output 派生的围栏 ```console 回落,工具自身不做二次编码。这正是决策记录中"围栏 console 输出"的主要生产方之一,也解释了为什么通用卡片的 Markdown 渲染路径必须能正确处理围栏。

备选方案与拒绝理由

决策记录列出了三个被否决的替代方案,其拒绝理由体现了该仓库的分层原则(展示器不依赖 UI 行为、UI 不特化某个生产方、字面语义不可被 Markdown 解释污染):

备选方案 拒绝理由
在 Bash 展示器中剥除围栏 只修复一个生产方,其他工具产生的通用卡片 Markdown 仍不会被渲染;还会让展示器依赖 TUI 的行为,破坏分层
把所有工具卡片都按 Markdown 渲染 终端输出和 diff 有专门格式,且可能包含必须保持字面显示的 Markdown 标点
在 Markdown 渲染之前应用收起状态卡片的行数限制 按源文本行截断可能把围栏块从中间截断,还会让可见行数与卡片使用的行数不一致

这三条否决理由与"决策"一节形成了完整的对照:修复点必须落在消费端(TUI 的渲染路径)且必须按渲染意图(card 类型)分流,Markdown 只解释"展示器撰写的行文",不解释"参数与字面输出"。

后果:统一 Markdown 词汇,按渲染意图分流

决策记录的 Consequences 部分固定了四条长期影响:

  1. 同一套词汇与净化路径:通用工具卡片与对话内容从此使用同一套 Markdown 词汇和净化(sanitization)路径。这意味着 Markdown 安全处理只需维护一份,工具卡片与对话内容天然一致。
  2. 标点语义变化:通用卡片中的 Markdown 标点会被解释,而不再总是按字面显示。这是一个有意的行为变更——它换取了视觉一致性,代价是通用卡片不再是纯文本容器。
  3. 字面输出的逃生通道:需要字面终端输出的工具,应使用终端卡片这一渲染意图card: 'terminal'),而不是把输出塞进通用卡片。渲染意图联合类型(ToolResultViewcard 字段)就是这条逃生通道的类型化表达。
  4. 测试覆盖:聚焦的 TUI 测试固定了三个行为点——隐藏的围栏、保留的语言标签、正文文本;无密钥(keyless)的终端状态快照通过组装后的 TUI transcript 端到端覆盖该行为。

仓库中的后续:TUI 移除后,哪些部分活了下来

理解这一决策在当前仓库中的适用边界,需要结合一条后续记录:Remove the TUI package(2026-08-04 落地)。该决策删除了 packages/ui/tui 包及其终端渲染器、快照 fixtures、补丁依赖与 SDK 脚手架,且明确说明归档的 TUI 实现笔记(包括本文对应的这份)是"历史冻结记录,不再是受支持包或应用清单的权威"。因此:

  • "共享 Markdown 主题隐藏围栏、保留语言标签、代码配色"这一具体渲染行为,属于已删除的 TUI 实现,在当前代码库中不可再直接验证,本文按决策记录的表述引用;
  • 仍然活在当前仓库中的部分是决策所依托的中性契约:card 打标签的渲染意图联合与 generic/terminal/diff 分工(packages/core/tools/src/presentation.ts)、头尾截断纯函数(packages/client/ui-primitives/src/head-tail-cap.ts)、以及"无能力 UI 回落到围栏 console 通用卡片"的 bridge 契约。现存产品界面(Web、ACP、JSON-RPC、一次性 CLI)中,Markdown 与围栏的处理依然延续着"展示器行文走 Markdown、参数与终端输出保字面"的同一套分流原则;Web 侧的围栏高亮行为甚至有专门的端到端 fixture,例如 apps/web/tests/streaming-fence-highlight.e2e.tsmid-stream.expected.md

小结

这份 2026-07-23 的决策记录给出的是一个可复用的界面渲染方法论,而不仅是一次 TUI 修复:

  • 修复点放在消费端(UI 渲染路径),而不是让每个生产方(工具展示器)各自适配终端行为;
  • 按渲染意图分流:展示器撰写的行文(通用卡片结果内容)走共享 Markdown 主题与净化路径;工具参数(rawInput)与专用格式(终端输出、diff)保持字面;
  • 先渲染、后截断:所有行数统计与头尾边界基于可见终端行计算,截断纯函数(headTailCap)只消费行数,由调用方保证传入的是渲染后的行数;
  • 行为变更必须写清代价:Markdown 标点从"总是字面"变为"被解释",同时提供渲染意图层面的逃生通道(终端卡片)。

TUI 包后来被整体移除,但上述契约中的中性部分(呈现意图联合、头尾截断、围栏回落)仍然构成 DeepSeek Harness 工具卡片体系的现行基础;对这份历史决策的复述与路径对照,可作为理解当前 presentation.tsui-primitives 卡片行为的设计出处参考。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384