DeepSeek Harness TUI 呈现层统一设计:唯一调色板表、状态优先工具卡片、与内容无关的折叠与跨工作区恢复
本文为 DeepSeek Harness(下称 dsh)终端界面(TUI)的一次架构级收敛决策做了完整解读。它说明了在 Everything is a Plugin 的插件化底座之上,终端阅读模型如何被收敛为四条可持续维护的规则:paletteSpec 唯一调色板表、状态标题加内收正文的工具卡片、按普通文本处理并仅依赖行数的注入上下文折叠,以及感知工作区(workspace)的 /resume 恢复导航。读完后,你将理解 dsh 终端样式契约的 SGR 语义、卡片与折叠状态的交互模型,以及恢复会话时 cwd 交接的进程级细节,并能在阅读仓库源码与测试时验证这套契约的落地方式。
问题的起点:多套呈现规则彼此干扰
在做出统一决策之前,终端 UI 逐步积累了多套互相冲突的呈现规则,原始决策文档(中文版,English,状态为 implemented,2026-08-04 归档)将其归纳为四类症状:
- 调色板角色互为别名:多个语义相近的角色实际渲染为相同效果,或在浅色终端中颠倒强调层级,读者无法建立稳定的视觉预期;
- 工具卡片噪声:卡片内部同时使用默认前景、青色命令、dim 的 cwd 行、无样式 XML 和 dim 输出,框架、输出与退出标记重复或争夺注意力;
- 注入上下文被当作 XML 解析:
<system-reminder>包裹的上下文里可能包含原始&、比较表达式和尖括号占位符,强行解析导致折叠不可靠; /resume工作区盲视:即使启动器能访问其他工作区,恢复选择器也会排除不属于当前工作区的会话,用户被迫手动重启。
文档的结论是:这些症状看似局部,但持久决策只有一个——一个终端阅读模型,即“精简且可检查的调色板、以状态为首且正文内收的卡片、与内容无关的记录折叠,以及感知工作区的导航”。下文逐节展开这四个支柱。
统一调色板:paletteSpec 是唯一事实来源
一张表、一套派生、一个打印命令
paletteSpec(scheme) 被定义为 SGR(Select Graphic Rendition)开始码、结束码与用途的唯一表。所有包装器(styler)由 createPalette 从这张表派生,任何组件都不允许自行发出 SGR 序列——唯一的例外是固定的启动品牌渐变。/palette 斜杠命令会在运行中的终端里打印同一张表,使样式契约在用户侧可检查、可核对,而不是只存在于源码里。
这意味着呈现规则的收敛点从“各组件各自硬编码转义序列”变成“单表派生”:新增或修改角色只需改表,派生逻辑与打印逻辑自动保持一致。
角色合并与 dim 的跨主题实现
重复角色被直接合并,消除“别名暗示并不存在的差异”:
| 变更 | 说明 |
|---|---|
muted 并入 dim |
两个角色实际效果一致,保留双名只会造成契约歧义 |
added 并入 success |
差异视图的“新增”与成功语义合一 |
removed 并入 error |
差异视图的“删除”与错误语义合一 |
| 移除未使用的第二强调色 | 减少表宽度,降低误用空间 |
dim 角色的实现细节尤为关键:它在两种配色方案(浅色/深色终端)中都使用 SGR 开始码 2;39,并以 22;39 结束。2(dim 属性)让内收文本相对于终端自身前景色变暗,而不是固定成某个深灰色;39(默认前景)保证颜色继承终端主题。组合的效果是:在浅色背景上 dim 文本只是“变淡”而不会变成一块看不清的深灰,在深色背景上同样成立。这正是“内收正文”在两种终端主题下都能成立的底层保证。
SGR 重置语义与 TypeScript 约束
每个结束码都重置对应开始码所设置的所有 SGR 组——这是终端转义序列最容易被踩坑的地方:一个不完整的结束序列会让内层样式泄漏到外层文本。dsh 的对策是把重置语义写成表契约,并在类型层面加约束:
- TypeScript 分别标记“颜色”和“属性”两类样式,允许属性与颜色自由组合(例如 dim 属性叠加深色前景);
- 同时拒绝“嵌套颜色”——内层颜色会因外层结束码的重置而丢失外层颜色,这类组合在编译期即被拒绝,而不是在运行时静默失败。
文档明确记录了这笔权衡:以少量编译期摩擦,换取对“静默样式丢失”的防护。对扩展作者有一个直接可见的后果:公共 TuiTheme 的 muted 角色被移除,扩展如需内收效果应改用 dim。
工具卡片:状态标题 + 统一内收正文
卡片结构原则
工具卡片被统一为两个视觉层:
- 一行带颜色的
Tool / <name>状态标题——它是扫描锚点,让用户在快速滚屏时先定位“哪个工具、什么状态”; - 一块统一的 dim 正文——呈现器标题、终端命令及 cwd 行、输出、XML 文本和折叠标记全部使用正文色调(即上一节
dim的2;39/22;39语义)。
这与仓库中工具呈现词汇表的类型设计一致。packages/core/tools/src/presentation.ts 中定义了与 UI 提供方无关的呈现意图(render-intent):TerminalCallView 声明“这是一条在某个 cwd 下运行的 shell 命令”,能力完整的 UI 渲染为带 cwd 头的终端卡片,能力不足的 UI 回退为通用卡片。对应的结果类型 TerminalResultView 携带捕获的输出 output、进程退出码 exitCode 与被信号终止时的 signal 名——注释中写明这“让能力完整的 UI 显示退出状态 pill(药丸标记)”。换言之,结构化状态(退出码/信号)由类型承载并只呈现一次,而不是依赖卡片正文里的文本标记。
语义颜色例外与模型侧标记的剥离
统一内收并不是一刀切:
- 差异(diff)颜色保留:红绿在差异视图中承载“删除/新增”的语义,是狭窄而明确的例外;
- 信号标记继续作为错误显示:进程被信号杀死属于错误事实,维持错误色调。
另一条关键边界是“面向模型”与“面向 UI”的分离:终端呈现器在返回 TerminalResultView.output 之前,先解析并移除面向模型的末尾退出或信号标记;TUI 只把结构化状态呈现一次。截断、超时和沙箱信息则继续留在正文中,因为状态标记并不表达这些事实。这样两类受众各看到一种表示:模型收到文本标记,终端用户看到结构化状态,二者不再重复。
注入上下文卡片:普通文本、精确配对移除与 Ctrl+O 三态
为什么不走 XML 树渲染器
注入上下文由 ContextCardComponent 按普通文本呈现,不再经过 XML 树渲染器。原因在决策文档中被讲得很清楚:提醒框架(reminder framing)只是包裹任意普通文本的提示约定,正文里天然会出现原始 &、比较表达式(<、>)和尖括号占位符——把它“修复”或转义成 XML,要么是在猜测结构,要么会改变模型可见文本。
因此折叠规则收窄为一条可判定规则:仅移除精确配对的外层 <system-reminder> 行;不匹配、单边(只有开标签或只有闭标签)、以及正文内部出现的类似标签,一律原样保留。面向模型的内容在这个过程中完全不变。
折叠行为本身也不再依赖解析成功与否:折叠在正文组装完成后进行,使用共享的 preview 辅助函数,因此只取决于行数,与载荷里包含哪些字符无关。这直接修复了旧实现中“特殊字符导致折叠不稳定”的问题。
Ctrl+O 三态循环
Ctrl+O 在折叠、展开和隐藏三个状态之间循环:
- 隐藏状态会连同卡片自有的前导间距一起移除工具卡片,避免残留空行;
- 上下文卡片参与折叠和展开状态,但工具进入隐藏状态时,上下文卡片回到折叠状态而不是被移除——因为注入指令不是可丢弃的工具流量,隐藏阶段只移除工具流量;
- 隐藏阶段还会把每个轮次(turn)的 assistant 步骤折叠为一条消息,该规则由另一份 Agent Note 隐藏模式 assistant 折叠 负责,本文不展开。
至此,一个快捷键完整控制了记录密度:长会话中用户可以在“细节全开 / 默认折叠 / 只留关键消息”之间循环切换,而无需理解任何解析内部状态。
跨工作区恢复:/resume 感知工作区
选择器范围与 cwd 校验
恢复选择器汇总所有工作区的记录,并维护一个可用 Tab 键切换的范围:当前工作区 / 所有工作区。默认范围是当前工作区;只有切到更宽范围时才显示工作区标签,避免当前工作区内被无关标签干扰。没有 cwd 的记录会被直接拒绝——没有可进入的目录,恢复就没有意义。
TuiResumeHost.handoff:cwd 必须在进程替换前交接
交接(handoff)的进程级细节是这个设计的硬核部分:
TuiResumeHost.handoff接收选中的SessionId和预检时重新读取的 cwd——预检而非选择时缓存,避免目录在选中后才失效;- CLI 在释放当前应用之前完成目录切换:若目录不可访问,切换会在此阶段失败,终端(当前应用)仍然可以恢复,而不是让用户陷入一个坏掉的半交接状态;
- 随后
execve替换进程并继承所选工作区; - 退出提示(exit hint)由启动器(launcher)提供,而不是让 TUI 反推启动器命令语法——TUI 不假设自己是如何被启动的。
文档特别强调了一条不变式:恢复的会话头里的 cwd 并不控制文件系统和 shell 的路径解析。因此在“启动后推断 cwd”的方案被否决——目标目录必须在进程替换之前,通过主机接口显式交接。相关的早期功能决策可参见 TUI resume 命令 Agent Note。
被否决的备选方案及理由
决策文档完整保留了否决记录,这是其“一份记录统一拥有最终规则”价值的体现。归纳如下:
| 备选方案 | 否决理由 |
|---|---|
| 为每个视觉症状保留独立 Agent Note 和局部修复 | 这些决策共享同一阅读层级且彼此多次取代;由一份记录统一拥有最终规则,读者无需重建变更顺序 |
| 保留别名,依靠约定执行呈现规则 | 别名暗示并不存在的差异;嵌套颜色重置或不完整的 SGR 结束会静默失败。单一表格加类型约束使契约可检查、可机械验证 |
| 保留工具卡片内部的框架/输出颜色分层 | 真实卡片会混用默认前景、青色命令、dim cwd、无样式 XML 和 dim 输出;状态标题已提供扫描锚点,统一内收正文消除噪声,差异颜色是狭窄的语义例外 |
| 把注入上下文继续解析或修复成 XML | 提醒框架只是包裹任意普通文本的提示约定;修复或转义要么猜测结构,要么改变模型可见文本 |
| 随工具卡片一起隐藏上下文卡片 | 上下文承载注入指令,不是可恢复的执行细节;隐藏阶段只移除工具流量 |
| 把恢复限制在一个工作区,或在启动后推断 cwd | 前者迫使用户手动重启;后者恢复的会话头 cwd 不控制文件系统和 shell 的路径解析。目标目录必须在进程替换前跨过主机接口 |
| 移除 TUI 退出状态标记,或移除面向模型的退出标记 | 前者是便于扫描的 UI 状态,后者是模型的状态信号;呈现器在构造结构化视图时消费文本标记,使两类受众各看到一种表示 |
影响面与残余限制
决策文档的 Consequences 部分明确了落地后果与已知边界,值得扩展开发者和维护者留意:
- 公共 API 变化:
TuiTheme的muted角色被移除,扩展改用dim;调色板和renderUnknownXml契约变得更严格,后者要求显式接收正文样式器。 renderUnknownXml契约:对未知工具结果的渲染,现在显式接收正文样式器作为参数,未知结果的样式不再依赖调用方的隐式约定。- 跨工作区恢复的路径语义:恢复会把所有依赖路径解析的工具移动到另一个目录;cwd 缺失或不可访问时不能完成交接。更宽的选择范围也使共享会话存储的并发访问更容易被触达,跨进程会话锁仍是独立后续工作。
- 退出标记残余限制:终端呈现器仍会把与退出标记语法完全一致的最后一行输出视为结构化状态——如果某个命令有意打印这种行,卡片正文可能丢失该行。文档注明这一残余限制已在
dsh-tool-bash的记录中被跟踪,读者可在该工具的包文档中查阅。
测试与验证策略
Testing 一节列出了这套契约的机械验证方式,与“契约可机械验证”的设计目标对应:
- TUI 单元测试 + 无密钥终端快照覆盖:调色板枚举、浅色/深色角色、合法与非法样式组合(对应上一节的类型约束)、统一 dim 卡片正文、保留语义的差异颜色、仅有一个退出状态且正文无标记、普通文本上下文框架、与内容无关的折叠、
Ctrl+O三态循环、模型过滤和两种恢复范围; - CLI 交接测试覆盖:传递重新读取的 cwd,并在释放当前应用前拒绝目录切换失败;
- tool-bash 测试把结果标记的生成、解析和移除固定为同一轮往返契约,防止面向模型与面向 UI 的两种表示在各自一侧被改动后失配。
小结
这份归档决策把 dsh TUI 的呈现层收敛为一个可检查的终端阅读模型:paletteSpec 单表派生加类型约束的调色板、状态标题加统一 dim 正文且差异色保留语义的工具卡片、精确配对移除 <system-reminder> 且仅依赖行数折叠的上下文卡片,以及 cwd 在 execve 前显式交接的跨工作区恢复。对插件与扩展作者,最直接的落地要点是:样式一律经由 createPalette 派生的包装器发出,内收文本使用 dim(muted 已不存在),未知工具结果渲染遵循 renderUnknownXml 的新契约。相关源码入口可参考 packages/core/tools/src/presentation.ts 中的工具呈现词汇表,以及决策文档中引用的两份 Agent Note(隐藏模式 assistant 折叠、TUI resume 命令),它们共同构成这条呈现链路在当前仓库中的完整证据链。
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 StartedRust0622
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