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

33 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent Note: Code Mode 的 chat 渲染——子调用作为父行之下的原生行
Status: implemented
[English](2026-07-26-code-mode-chat-subcall-rows.md) | 中文
> 范围:Web chat 视图如何渲染一个 `run_code` 轮次,即 Code Mode UI 堆叠 PR(Pull Request)链的客户端侧部分,构建在[宿主侧基础](2026-07-26-code-dispatch-ui-foundation.md)之上(携带完整内容的 `tool/code-dispatch`、必填的 `description` 参数)。本篇所依托的 slot 模型归 [toolview 溶解](../architecture/2026-07-23-toolview-dissolution.md)所有。
## 问题
启用 Code Mode 后,chat 视图过去只显示一条不透明的 `run_code` 行:摘要就是原始程序文本,子调用则处处不可见。已敲定的产品要求恰恰相反:每个子调用都必须与原生工具调用渲染得*完全一致*——同样的行组件、同样的自定义注册、同样的详情面板——同时 transcript(文本记录)仍须如实反映模型只发起了一次调用这一事实。
## 决策
**子调用在界面流之外单独索引为 `ToolResultNode`,经由与原生行相同的 keyed slot 渲染,以始终可见的方式嵌套在父行之下。**
- **数据层**:`Session.applyEventSideEffects` 把窗口内的每条 `tool/code-dispatch` 折入 `ConversationSnapshot.codeDispatches: ReadonlyMap<parentCallId, readonly CodeSubCall[]>`,其中 `CodeSubCall` 本身就是 `ToolResultNode`(子调用 id 充当 `callId`,已记录的参数经 JSON 字符串化写入 `call.argsRaw`,完整记录的 `content`/`isError` 原样携带)。实时多路复用帧与历史回放构建出同一份索引(`rebuildDerivedFromWindow` 先清空再重新推导;逐父级的写时复制(copy-on-write)数组保持快照引用稳定,便于 memo 化)。子调用永不进入 `nodes`——surface 流始终精确等于模型可见的轮次结构。该事件在 wire 消费方边界作结构性收窄(dsh-tools 的宿主类型无法进入客户端程序——宿主端/客户端两侧的 `Context` 声明合并会冲突),姿态与所有跨 wire 载荷一致。
- **渲染层**:`ChatView` 的 `CallRow` 先渲染父行,随后对索引中出现的父级渲染一组 `[data-subcalls]` 嵌套的 `SubCallRow`,每一行都经由同一个 `'conversation.chat.toolview'` keyed slot、以 `entryKey = sub-tool name` 分发,并共用同一个 `GenericToolCard` 后备组件。与原生行的同一性由构造保证:一个 keyed 注册(例如 bash 样例)接管子行与接管顶层行的方式完全相同,注册本身零改动。运行中的父调用(`runningCalls`)也以同样的方式嵌套目前已产生的分发,因此子行在运行期间实时流入(PR1 在每次分发完成时即记录该分发)。
- **`run_code` 的呈现**:新增一种 `code` 行变体(分类器映射 `run_code → code`、标题 `Code`、图标 `IconCodeOutline16`),以模型撰写的 `description` 作摘要,展开后显示程序本身(在 markdown 代码块的填充底色上以等宽字体呈现),而非参数的 JSON 封装。
- **详情面板**:`materialFor` 按 nodes → runningCalls → 分发索引的顺序逐级回落,因此被选中的子调用 callId 会经由与已完结的原生调用完全相同的渲染路径,解析出完整参数与完整输出。
## 曾考虑的替代方案
**把子调用平铺进 surface 流(折入 `nodes`)。** 否决:这会歪曲 transcript——模型只发起了一次调用;嵌套在父行之下既保住代码↔调用的关联,也让折叠过程的模型可见顺序不变式原封不动。
**隐藏子调用,展开父行后才显示。** 由产品决策否决:子调用正是一个 Code Mode 轮次的核心内容;把它们藏起来,等于重新制造出本功能所要消除的那种不透明。父行的展开开关只用于显示程序本身。
**专用的子调用行组件。** 否决:本功能的全部要义就在于与原生行保持同一性;一个平行组件必然漂移。嵌套包装层(缩进 + 左侧边线)是子调用唯一的专属视觉装饰。
## 后果
自定义 toolview 注册免费适用于子调用——而且是刻意为之:不存在按注册粒度的退出机制,唯一的出路是组件自行读取自身上下文,而当前没有任何消费方需要这么做。选中高亮经由同一条 `selectedCallId` 通道到达嵌套行(分组归属判断会同时检验两个层级)。trajectory/waterfall 仍把 `run_code` 渲染为单独一行——它们的子调用 span 推迟到增加分发计时(start/end 事件)的那个 PR;缺少计时,waterfall 上的 span 就是在撒谎。fixture(测试前置数据)的轮次 64(`?fixture`),加上 `code-mode-round` 浏览器 e2e(录制的真实轮次、无密钥回放),共同锁定整个界面;jsdom 测试套件则锁定 slot 分发、错误状态、详情解析与索引引用稳定性。