DeepSeek Harness 终端工具卡片修复:单行字段内联转义,根治多行命令引发的渲染覆盖
本文剖析 DeepSeek Harness(gh_mirrors/de/deepseek-harness)中一个典型的终端行式 UI 缺陷:bash 工具的多行命令直接进入工具卡片标题后,因换行符未被转义而越出行数核算预留的区域,覆盖描述、输出与提示行。围绕归档缺陷笔记 2026-07-27-tool-card-single-row-fields-inline.md(中文对照),本文还原其完整的故障链条、displayInlineText 与 displayText 的分野、被否决的替代方案,以及该决策在 TUI 包移除后的历史定位与可迁移设计经验。读完后,你将掌握终端行式渲染中"逻辑行核算"与"物理行输出"不一致这类 bug 的定位方法,以及单行/多行字段转义策略的正确落位。
一、缺陷背景:工具卡片是"按行核算"的复合组件
DeepSeek Harness 的终端界面(dsh TUI)以"工具卡片"作为一次工具调用的展示单元。一张卡片由若干逻辑行组成:标题(含状态字形 ◌ / ✕ / ✓)、description 元数据行、cwd 元数据行、待执行时的 $ <command> 回显行、命令输出区,以及退出码行。终端渲染器按"逻辑行数"为每个字段预留屏幕行,渲染时逐行覆写预留区。
这套模型有一个隐含契约:声明为单行的字段,实际输出必须恰好是一行。缺陷的根源正是这个契约被 bash 工具的特殊性打破。
二、问题链条:多行命令如何把卡片渲染成乱码
按笔记 Problem 小节 的完整描述,故障链条有四环:
- 字段来源:卡片标题、描述、
cwd与$ <command>回显各自都是一个逻辑行; - 触发点:bash 工具直接把模型给出的命令文本与描述文本设置为卡片标题和描述。模型产出的 bash 脚本常常是多行的(例如
S=/tmp换行echo $S),其中包含真实换行符\n; - 错误转义:这些字段此前用
displayText做转义,而displayText的设计意图是刻意保留\n作为结构性布局——它服务于"本就该占多行"的内容(如命令输出); - 核算失配:多行标题因此渲染成多条终端行,而卡片的行数核算只为标题预留了一行。标题溢出的行落在未预留的区域上,把下方的描述行、输出行,甚至编辑器的 steering 提示行直接覆盖掉,卡片表现为互相重叠的乱码文本。
值得注意的是缺陷的暴露时点:同批归档的简化笔记 2026-07-27-copyable-transcript-no-gutter-bar.md(中文对照)记录了 TUI 移除逐行左侧 gutter bar(▌ 前缀)的决策。此前每行都有逐行前缀,覆盖冲突被前缀"垫"在视觉后面而不显眼;gutter bar 移除、正文退到第 0 列后,行与行之间的碰撞才完全暴露。这也提示一个经验:UI 简化(去装饰)往往会同时放大潜伏已久的布局 bug。
三、修复决策:按"单行/多行"分级选择转义函数
Decision 小节 给出的决策是把转义函数按字段的行语义分级使用:
| 字段 | 行语义 | 修复后使用的转义 | 效果 |
|---|---|---|---|
| 卡片标题 | 单行 | displayInlineText |
\n → 字面量 \x0a,恒占一行 |
terminal 卡片 description 元数据行 |
单行 | displayInlineText |
同上 |
terminal 卡片 cwd 元数据行 |
单行 | displayInlineText |
同上 |
待执行的 $ <command> 回显 |
单行 | displayInlineText |
同上 |
| 捕获的命令输出 | 真·多行 | displayText + split('\n') |
逐行渲染,占多行 |
contentText 结果正文 |
真·多行 | displayText + split('\n') |
逐行渲染,占多行 |
核心原则只有一句话:转义函数必须匹配字段的行契约。单行字段用 displayInlineText(把 \n 转义为字面量 \x0a,让换行在终端上"可见但不可执行");真正多行的字段保留 displayText 加 split('\n'),因为它们理应占据多行、行数核算也为其预留了空间。修复后每个单行字段严格保持一行,多行命令不再有能力"越行"。
一个细节佐证该设计的意图:转义产物是字面量文本 \x0a(两个字符 \ 与 x0a 的可视化序列),而非删除换行。多行命令 S=/tmp\necho … 渲染为单行标题 S=/tmp\x0aecho …——模型的命令形态(包括它的分行结构)对阅读者仍然可辨,只是不再产生布局副作用。
四、被否决的两个替代方案及其理由
笔记 Alternatives 小节 明确记录了两个被否决的方案,两者都体现了同一设计立场:UI 转义责任应落在渲染处,而不是数据生产处。
方案一:在 bash 工具的 presenter 输出中剥除换行。 否决理由有二。其一,presenter 输出是"模型真实命令"的忠实镜像,剥除换行等于对该视图的所有消费方(终端卡片、transcript、潜在的自动化读取方)隐藏了命令的真实形态;其二,这会把一个纯 UI 关注点(如何在一行里呈现换行)压进工具实现里——一旦未来另一个宿主以多行方式展示该字段,数据已经损坏,无法恢复。
方案二:让标题刻意换行到多行。 否决理由:卡片标题是"一行式身份标识"(one-line identity),允许它换行后,除非重排整个卡片并让所有下游字段的行数核算跟着标题实际行数联动,否则它照样和紧随其后的元数据行冲突;此外多行标题还会无谓地膨胀 transcript。换言之,方案二是用更大的改动面去换一个视觉上略"美观"、但本质上仍是布局负债的结果。
两个否决项合起来回答了一个更普遍的问题:缺陷出现在哪个环节,就该在哪个环节修? 答案是"出现在渲染契约被违反的环节,就修渲染契约"——而不是在上游污染数据,也不是把契约放宽到容纳违约。
五、验证方式:双状态 tmux 实测与 spec 断言
Consequences 小节 给出了两条验证证据:
- 端到端实测:多行 bash 命令渲染为单行内联标题(
S=/tmp\x0aecho …),其下的描述行、输出行与退出码行保持完整;在 tmux 中分别对待执行(◌)与已完成(✓)两种卡片状态做了实测。之所以两种状态都要验,是因为待执行态还包含$ <command>回显行这条独立的单行字段路径——它和标题一样被换行污染,也必须被同一次修复覆盖。 - 回归测试:
tui.spec.ts中新增multilineTerminal工具卡片用例,断言对含换行符的标题与描述,渲染结果中出现内联转义后的形式(即字面量\x0a而非真实换行)。这保证"换行不越行"成为被 CI 守护的行为契约,而不是仅靠人眼在 tmux 里确认。
六、历史定位:TUI 包移除后的冻结记录与可迁移经验
必须说明该决策的当前适用性:按实施记录 2026-08-04-remove-tui-package.md,packages/ui/tui 包已于 2026-08-04 整体删除——终端渲染器、tui.spec.ts、快照夹具与配套依赖一并移除,笔记中引用的 displayInlineText / displayText 实现与 multilineTerminal 用例随之离开仓库,DeepSeek Harness 现存的交互式界面为 Web(可参见 ui-tool 模块),ACP、JSON-RPC 与一次性 CLI 保留为非 Web 入口。因此本篇笔记(Archived: 2026-08-04,Status: implemented)是冻结的历史记录,不再是对应现存代码的权威说明;该移除决策也明确声明,归档的 TUI 笔记"不再是受支持包与应用清单的权威"。
不过,笔记的方法论价值并不随代码消失。可以归纳出三条对任何"自绘行、按行覆写"式终端渲染器仍然成立的通用经验:
- 字段的行契约要显式分级:凡是声明单行的字段,数据源不可信(模型输出尤甚),渲染处必须有一个"不可换行"的转义通道;
- 行数核算与物理输出必须对齐:布局器按 N 行预留,渲染就必须保证 N 行以内,任何字段都不应隐式地向"下方邻居"借行;
- 转义发生在渲染处,保真发生在数据处:上游保原始数据完整,UI 在最后一英里做可见化转义(
\n→\x0a),既不让消费方丢失信息,也不让工具层沾染布局职责。
作为对照,可以推断:当前 Web 端工具卡片渲染在 DOM 流式布局下,多行命令文本会自然撑高卡片高度,不存在"行核算"概念,因此同类覆盖冲突在该渲染模型下不会发生——这也从侧面说明本修复解决的是行式终端渲染特有的问题,其教训应在下一个终端前端(无论是否为 Harness 官方实现)重新落地时提前规避,而非再次等到 gutter bar 这类简化把冲突"照"出来。
参考
- 缺陷笔记(英/中):2026-07-27-tool-card-single-row-fields-inline.md · 中文对照
- 关联简化笔记:2026-07-27-copyable-transcript-no-gutter-bar.md · 中文对照
- TUI 包移除决策:2026-08-04-remove-tui-package.md · 中文对照
- 现存工具卡片渲染模块:packages/client/ui-tool
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