DeepSeek Harness:统一 TUI 呈现与导航——一套终端阅读模型如何收敛调色板、工具卡片、上下文折叠与跨工作区 Resume
本篇基于 DeepSeek Harness(gh_mirrors/de/deepseek-harness)仓库中的架构决策笔记 2026-07-28-consolidated-tui-presentation.md 展开:它记录了终端 UI(TUI)如何将多套彼此冲突的呈现规则——调色板角色别名、工具卡片框架重复、注入上下文被当作 XML 解析、/resume 仅限当前工作区——收敛为一套单一、可检查的"终端阅读模型"。读完后,你将理解 paletteSpec(scheme) 单表调色板的设计契约、工具卡片"状态标题 + 内收正文"的结构、Ctrl+O 三态折叠机制,以及跨工作区 resume 时 cwd 如何在进程替换前穿过 host 接缝,并掌握其配套测试策略与已知残留边界。
背景问题:局部修复为何会不断互相推翻
原 Agent Note 将问题归纳为四个独立症状,但它们共享同一个根因——缺少统一的终端阅读模型:
- 调色板角色互为别名或在浅色终端上颠倒强调:
muted、dim、added、removed等角色语义重叠,导致组件各自为政地选择强调方式; - 工具卡片的框架、输出和退出标记重复或争夺注意力:卡片内混用默认前景色、青色命令、dim 的 cwd、无样式 XML 与 dim 输出;
- 注入上下文被当作 XML 解析,无法可靠折叠:
<system-reminder>包裹的其实是任意散文(含裸&、比较符、占位尖括号),强行解析必然失败或改动模型可见文本; /resume排除当前工作区之外的会话:即使启动器本来可以到达那些会话。
原文的核心论断是:每个症状看似局部,但持久决策只有一个终端阅读模型——精简且可检查的调色板、以状态为首且正文内收的卡片、与内容无关的记录折叠、感知工作区的导航。这个决策还直接回应了仓库中另一条决策:隐藏模式下把每轮 assistant 步骤折叠为一条消息的规则由 2026-07-29-tui-hidden-mode-assistant-fold.md 单独承载,两者共同构成"终端阅读模型"的完整版图。
调色板:一张表统治所有 SGR 序列
契约设计
统一后的调色板契约是:
paletteSpec(scheme)是 唯一的 SGR 开始码、结束码与用途表;createPalette从该表派生出所有样式包装器;/palette斜杠命令在运行中的终端里打印的正是同一张表,保证"你看到的就是代码里的";- 除固定的启动品牌渐变(见 2026-07-21-tui-banner-brand-gradient.md)外,组件不允许自行发出 SGR 序列;
- 每条结束码必须重置对应开始码设置的所有 SGR 组,杜绝"结束码不完整导致样式泄漏"这类静默失败。
角色合并与 dim 的相对化
重复角色被合并,规则明确:
| 原角色 | 合并去向 | 理由 |
|---|---|---|
muted |
dim |
语义与 dim 完全重叠 |
added |
success |
diff 新增色与成功色同义 |
removed |
error |
diff 删除色与错误色同义 |
| 第二个强调色(未使用) | 直接移除 | 没有消费方 |
其中 muted 被并入 dim 后,公开的 TuiTheme.muted 角色被整体移除,扩展方需要使用 dim——这是对下游插件的破坏性变更,但换来角色集合的无歧义。
dim 的关键细节在于 SGR 取值:两种配色方案(dark/light)下统一使用 2;39 开始、22;39 结束。39(默认前景色)加上 2(减淡)意味着"内收文本"是相对终端前景色变暗的,而不是某个固定的深灰——这样在浅色终端上不会退化成"背景接近深灰、对比度崩塌"的老问题。
TypeScript 层面的组合规则
颜色(color)与属性(attribute)在 TypeScript 类型中被分别标记。这带来两条机械约束:
- 允许"属性 + 颜色"自由组合(例如减淡 + 青色);
- 拒绝嵌套颜色——因为内层颜色的结束码会把外层颜色一并重置,造成静默丢色。
原 Note 明确把这称为"以少量编译期摩擦换取防止静默样式丢失":paletteSpec 和 renderUnknownXml 的契约变严格了,扩展作者写 TUI 组件时多一层类型阻力,但非法组合在编译期即被拒绝。
工具卡片:一个状态标题 + 一块内收正文
卡片结构
统一后的工具卡片只有两层结构:
Tool / <name> ← 唯一的彩色状态标题(scan anchor)
─────────────────────
<正文,全部 dim> ← 呈现器标题、终端命令、cwd 行、
输出、XML 文本、折叠标记
即:一行带颜色的 Tool / <name> 状态标题,下面一块统一的 dim 正文。卡片内不再区分"框架色/输出色"——所有非语义内容共享正文色调。两个明确的语义例外被保留:
- diff 颜色保留:红/绿承载增删语义,是"窄语义例外";
- 信号标记保留为错误样式。
这条决策直接推翻了早期卡片实现(可参见相关决策链:2026-07-27-tui-tool-card-header.md 定义了卡片头,2026-07-27-tool-card-single-row-fields-inline.md 修复了单行字段换行)——旧卡片混用五种色调,而状态标题本身已经是扫描锚点,正文只剩"内收"一种职责。
renderUnknownXml 与退出标记的去重
工具结果中的未知 XML 文本通过 renderUnknownXml 渲染,该函数显式接收一个正文样式器(而非内部自选颜色),把"未知结果如何着色"也收编进正文色调契约。
退出/信号标记的处理是另一个易错点:面向模型的最终退出或信号标记(文本标记)与 TUI 的状态 pill 是同一状态的两个受众。处理规则是:
- 终端呈现器(presenter)在返回
TerminalResultView.output之前,解析并移除模型可见的末尾退出/信号标记; - TUI 把该结构化状态作为自己的 pill 只渲染一次;
- 截断、超时、沙箱行留在正文里,因为 pill 不表示这些事实。
由此模型与 UI 各拿到一份"单一表示",不再出现 pill 与文本标记重复显示。相关背景可参考 2026-07-24-tui-turn-end-stop-reason-notices.md。
已知残留边界
原 Note 的 Consequences 明确记录了一个残留风险:终端呈现器仍会把"恰好与其退出标记语法精确匹配的最后一行输出"当作结构化状态处理,因此一条故意打印这种行的命令,会丢失其在卡片正文中的这一行。该残留在 dsh-tool-bash 技能中单独做了文档化。写测试脚本或调试 bash 工具输出时需要注意这一条边界。
注入上下文与折叠:按散文处理,按行数折叠
为什么不能按 XML 解析
注入上下文(injected context)曾被 XML 树渲染器处理,但 <system-reminder> 帧本质是"围绕任意散文的提示约定",正文中天然包含裸 &、</> 比较符、占位尖括号。任何修复或转义要么是在猜测结构,要么改动了模型可见文本。因此决策是:
- 注入上下文改由
ContextCardComponent按普通散文(prose)呈现,不经过 XML 树渲染器; - 只剥离精确配对的外层
<system-reminder>行——不匹配、单边(unpaired)或行内类似标签的文本原样保留; - 面向模型的内容完全不变。
与内容无关的折叠
折叠(folding)发生在正文组装之后,使用共享的 preview 辅助函数,因此它只取决于行数,从不依赖解析是否成功、也不关心载荷里包含什么字符。这保证了含裸 &、尖括号的任意散文都能稳定折叠,不会出现"解析失败 → 折叠失效 → 卡片撑爆屏幕"的连锁反应。
Ctrl+O 三态循环
Ctrl+O 在三个状态间循环,各状态对卡片的处理规则不同:
| 状态 | 工具卡片 | 上下文卡片 |
|---|---|---|
| collapsed | 折叠 | 参与折叠 |
| expanded | 展开 | 展开 |
| hidden | 连同卡片自有的前导间距一起移除 | 回落到折叠态 |
上下文卡片在 hidden 状态下"回落折叠"而不是"一并隐藏",理由是:注入的是指令,不是可丢弃的工具流量——工具卡片承载的是可恢复的执行细节,隐藏后不影响理解;注入指令则必须保留。
hidden 阶段还叠加了第二层折叠规则:把每轮 assistant 的步骤折叠为一条消息。该规则由专门的 Agent Note 2026-07-29-tui-hidden-mode-assistant-fold.md 承载:每轮中第一个有可见内容的步骤独占该轮唯一的 Assistant 头,其余步骤作为无头续段渲染,纯工具步骤不占头也不留空段;折叠态是重算值而非存储值,因此会话、持久化格式均不变。
跨工作区 Resume:cwd 必须在进程替换前穿过 host 接缝
选择器行为
/resume 选择器总结全部会话记录,并拥有一个用 Tab 切换的"当前工作区 / 全部工作区"作用域:
- 默认作用域为当前工作区;
- 只在"全部工作区"作用域下追加工作区标签(当前工作区内加标签是噪音);
- 拒绝没有 cwd 的记录——因为没有可进入的目录,选择即失败。
交接(handoff)机制
TuiResumeHost.handoff 接收两个输入:选中的 SessionId,以及 preflight 阶段重新读取的 cwd。交接顺序是刻意的:
- CLI 在销毁当前应用之前先切换目录;
- 目标目录不可达时在终端仍可控的窗口内失败(而不是在拆除过程中失败);
execve替换进程后自然继承所选工作区;- 退出消息由启动器(launcher)提供,而不是让 TUI 去重建 launcher 语法——避免 TUI 侧硬编码 launcher 命令行格式。
被否决的替代方案中有一条值得注意:"保持 resume 限定单工作区,或在启动后推断 cwd"被拒绝,理由是:限定单工作区迫使用户手动重启,而"恢复的头部 cwd 不控制文件系统与 shell 的解析"——目标目录必须穿过 host 接缝、在进程替换之前落实。
替代方案与最终取舍
原 Note 完整列出了七个被否决的替代方案及其理由,是这份决策最有参考价值的部分:
| 替代方案 | 否决理由(要点) |
|---|---|
| 为每个视觉症状保留独立笔记与局部修复 | 决策共享同一套阅读层级且反复互相推翻;单一负责人让最终规则可查,读者不必重建时间线 |
| 保留别名、靠约定约束呈现 | 别名暗示了不存在的区分;嵌套颜色重置或不完整 SGR 结束会静默失败;单表 + 类型使契约可检查、可机械校验 |
| 工具卡片内保留框架/输出颜色分层 | 真实卡片混用五种色调;状态标题已是扫描锚点,一块内收正文即可去噪;diff 颜色是唯一的窄语义例外 |
| 按 XML 解析或修复注入上下文 | reminder 帧是围绕任意散文的提示约定;修复/转义要么猜结构、要么改动模型可见文本 |
| 让上下文卡片随工具卡片一起隐藏 | 上下文承载注入指令而非可恢复执行细节;hidden 阶段应只移除工具流量 |
| resume 限定单工作区或启动后推断 cwd | 前者强制手动重启;后者让恢复的头部 cwd 不控制文件系统/shell 解析;目标目录必须在进程替换前穿过 host 接缝 |
| 去掉 TUI 退出 pill 或去掉模型可见退出标记 | pill 是可扫读的 UI 状态,文本标记是模型的状态信号;呈现器在构建结构化视图时消费标记,让两个受众各得一份表示 |
影响、约束与测试覆盖
对扩展作者的影响
- 公开
TuiTheme.muted角色被移除,扩展需改用dim; paletteSpec与renderUnknownXml契约更严格——以少量编译期摩擦换取防止静默样式丢失;- 组件一律不得自行发射 SGR(启动品牌渐变除外),
/palette是唯一的运行期调色板自省入口。
行为约束与风险
- 跨工作区 resume 可以把所有路径解析型工具挪到另一个目录;cwd 缺失或不可达会阻止交接;
- 全量选择器让"并发访问共享会话存储"更容易发生,跨进程会话锁被明确标记为独立的后续工作(未在本决策范围内);
- bash 输出末行恰好命中退出标记语法时会被结构化消费(见上文残留边界)。
测试策略
原 Note 的 Testing 一节列出了覆盖矩阵,全部针对上述契约而非视觉像素:
- TUI 单测 + 无按键终端快照:调色板枚举、明暗两方案的角色、合法/非法样式组合、统一 dim 的卡正文、语义 diff 颜色、无标记终端输出 + 单退出 pill、散文保留的上下文帧、与内容无关的折叠、
Ctrl+O三态循环、模型过滤、resume 的两种作用域; - CLI 交接测试:传递重读的 cwd;在拆除前拒绝目录进入失败;
- Tool-bash 测试:把结果标记的"发射 → 解析 → 剥离"钉死为一次完整往返(round trip),防止两端各改各的。
小结
这份已归档的架构决策(Status: implemented,Archived: 2026-08-04)的核心价值在于用一个可检查的终端阅读模型替代了 N 个局部约定:
- 一张
paletteSpec(scheme)表 + 类型化组合规则,让 SGR 契约在编译期与运行期(/palette)双重可验证; - 工具卡片收敛为"彩色状态标题 + 统一 dim 正文",diff 红绿与错误信号是唯一例外;
- 注入上下文按散文处理、按行数折叠,摆脱对载荷字符的依赖;
Ctrl+O三态循环 + hidden 阶段的 assistant 折叠(由 2026-07-29-tui-hidden-mode-assistant-fold.md 承载)统一控制记录密度;- 跨工作区 resume 通过 preflight 重读 cwd 与启动器提供的退出消息,把"选目录"落实在进程替换之前。
需要说明的是:该 Note 处于仓库 .agents/notes/archived/ 归档区,其指向的 TUI 呈现层实现(paletteSpec、createPalette、ContextCardComponent、TuiResumeHost 等符号)在当前仓库快照中未检索到对应源码文件,本文所有实现细节均以该决策笔记及其引用的关联 Agent Note 为事实依据;若要跟踪这些契约的最新形态,建议从 docs/subsystems/ 下与 web-client、session 相关的子系统文档,以及 .agents/notes/ 目录中相邻的 TUI 决策链(如 2026-07-28-tui-chat-channel-module-split.md、2026-07-24-configurable-tui-prompt-theme.md)继续阅读。
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