Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-26-code-mode-chat-subcall-rows.zh.md
T

4.8 KiB
Raw Blame History

Agent NoteCode Mode 的 chat 渲染——子调用作为父行之下的原生行

Status: implemented

English | 中文

范围:web chat 视图如何渲染一个 run_code 轮次,即 Code Mode UI 堆叠 PRPull Request)链的 client 侧一半,构建在宿主侧基础之上(携带完整内容的 tool/code-dispatch、必填的 description 参数)。本篇所依托的 slot 模型归 toolview 溶解所有。

问题

启用 Code Mode 后,chat 视图过去只显示一条不透明的 run_code 行:摘要就是原始程序文本,子调用则处处不可见。已敲定的产品要求恰恰相反:每个子调用都必须与原生工具调用渲染得完全一致——同样的行组件、同样的自定义注册、同样的 details 面板——同时 transcript(文本记录)仍须如实反映模型只发起了一次调用这一事实。

决策

子调用是 surface 流之外单独索引的 ToolResultNode,经由与原生行相同的 keyed slot 渲染,以始终可见的方式嵌套在父行之下。

  • 数据层Session.applyEventSideEffects 把窗口内的每条 tool/code-dispatch 折入 ConversationSnapshot.codeDispatches: ReadonlyMap<parentCallId, readonly CodeSubCall[]>,其中 CodeSubCall 本身就是 ToolResultNode(子调用 id 充当 callId,已记录的参数经 JSON 字符串化写入 call.argsRaw,完整记录的 content/isError 原样携带)。live mux 帧与历史回放构建出同一份索引(rebuildDerivedFromWindow 先清空再重新推导;逐父级的写时复制(copy-on-write)数组保持快照引用 memo 稳定)。子调用永不进入 nodes——surface 流始终精确等于模型可见的轮次结构。该事件在 wire 消费方边界作结构性收窄(dsh-tools 的 host 类型进不了 client 程序——host/client 两侧的 Context 声明合并会冲突),姿态与所有跨 wire 载荷一致。
  • 渲染层ChatViewCallRow 先渲染父行,随后对索引中出现的父级渲染一组 [data-subcalls] 嵌套的 SubCallRow,每一行都经由同一个 'conversation.chat.toolview' keyed 孔位、以 entryKey = sub-tool name 分发,并共用同一个 GenericToolCard fallback。与原生行的同一性由构造保证:一个 keyed 注册(例如 bash 样例)接管子行与接管顶层行的方式完全相同,注册本身零改动。运行中的父调用(runningCalls)也以同样的方式嵌套目前已产生的分发,因此子行在运行期间实时流入(PR1 在每次分发完成时即记录该分发)。
  • run_code 的呈现:新增一种 code 行变体(分类器映射 run_code → code、标题 Code、图标 IconCodeOutline16),以模型撰写的 description 作摘要,展开后显示程序本身(在 markdown 代码块的填充底色上以等宽字体呈现),而非参数的 JSON 信封。
  • details 面板materialFor 按 nodes → runningCalls → 分发索引的顺序逐级回落,因此被选中的子调用 callId 会经由与已完结的原生调用完全相同的渲染路径,解析出完整参数与完整输出。

曾考虑的替代方案

把子调用平铺进 surface 流(折入 nodes)。 否决:这会歪曲 transcript——模型只发起了一次调用;嵌套在父行之下既保住代码↔调用的关联,也让 fold 的模型可见顺序不变式原封不动。

隐藏子调用,展开父行后才显示。 由产品决策否决:子调用正是一个 Code Mode 轮次的核心内容;把它们藏起来,等于重新制造出本功能所要消除的那种不透明。父行的展开开关只用于显示程序本身。

专用的子调用行组件。 否决:本功能的全部要义就在于与原生行保持同一性;一个平行组件必然漂移。嵌套包装层(缩进 + 左侧边线)是子调用唯一的专属 chrome。

后果

自定义 toolview 注册免费适用于子调用——而且是刻意为之:不存在按注册粒度的 opt-out,唯一的出路是组件自行读取自身上下文,而当前没有任何消费方需要这么做。选中高亮经由同一条 selectedCallId 通道到达嵌套行(分组归属判断会同时检验两个层级)。trajectory/waterfall 仍把 run_code 渲染为单独一行——它们的子调用 span 推迟到增加分发计时(start/end 事件)的那个 PR;缺少计时,waterfall 上的 span 就是在撒谎。fixture(测试前置数据)的轮次 64(?fixture),加上 code-mode-round 浏览器 e2e(录制的真实 round、无密钥回放),共同锁定整个表面;jsdom 套件则锁定 slot 分发、错误状态、details 解析与索引引用稳定性。