# 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（文本记录）仍须如实反映模型只发起了一次调用这一事实。

## 决策

**子调用是在 surface 流之外递归附着到父级的标准 Tool 调用块，经由与原生行相同的 keyed slot 渲染，并始终显示在父级之下。**

- **数据层**：运行时的 `ToolCallTree` 把窗口内的 `tool/code-dispatch-start` 与 `tool/code-dispatch` 事件折入私有的逐父级索引，再把运行中和已结算的子级投影到递归的 `ToolCallBlock.subCalls` 上。实时 Session 投影与 `projectConversationHistory` 共享这一折叠过程；逐父级的写时复制数组和路径复制投影让无关 root 与 sibling 保持引用稳定。子调用永不进入 `nodes`——surface 流始终精确等于模型可见的轮次结构。这些事件在 wire 消费方边界作结构性收窄，该边界也会拒绝成环的父子关系（dsh-tools 的宿主类型无法进入客户端程序，因为宿主端与客户端两侧的 `Context` 声明合并会冲突）。
- **渲染层**：`ChatView` 通过整体 Tool seat `'conversation.chat.tool'` 传递每个 parent 及其递归 child。ui-tool 的 `ToolCallTree` 先渲染 parent，再渲染 `[data-subcalls]` 嵌套；每个原子调用都通过同一个 `'tool.call.toolview'` keyed slot，以 Tool 名称作为 `entryKey`，并共用 `GenericToolCard` fallback。一个 keyed 注册因此无需变化即可同时接管任意后代与顶层调用。运行中的 parent（`runningCalls`）在同一个递归块中接收已累积的 dispatch，使 child 行在运行期间实时流入。
- **`run_code` 的呈现**：新增一种 `code` 行变体（分类器映射 `run_code → code`、标题 `Code`、图标 `IconCodeOutline16`），以模型撰写的 `description` 作摘要，展开后显示程序本身（在 markdown 代码块的填充底色上以等宽字体呈现），而非参数的 JSON 封装。
- **详情面板**：`materialFor` 递归搜索 `nodes` 与 `runningCalls`，因此被选中的后代 callId 会经由与已完结的原生调用完全相同的渲染路径，解析出完整参数与完整输出。

## 曾考虑的替代方案

**把子调用平铺进 surface 流（折入 `nodes`）。** 否决：这会歪曲 transcript——模型只发起了一次调用；嵌套在父行之下既保住代码↔调用的关联，也让折叠过程的模型可见顺序不变式原封不动。

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

**专用的子调用行组件。** 否决：本功能的全部要义就在于与原生行保持同一性；一个平行组件必然漂移。嵌套包装层（缩进 + 左侧边线）是子调用唯一的专属视觉装饰。

## 后果

自定义 toolview 注册免费适用于子调用——而且是刻意为之：不存在按注册粒度的退出机制，唯一的出路是组件自行读取自身上下文，而当前没有任何消费方需要这么做。选中高亮经由同一条 `selectedCallId` 通道到达嵌套行（分组归属会搜索整棵树）。trajectory/waterfall 现在依据分发计时事件对（[实时并行分发](2026-07-26-code-mode-live-parallel-dispatch.md)）绘制子调用 span；缺少计时，waterfall 上的 span 就是在撒谎。fixture（测试前置数据）的轮次 64（`?fixture`），加上 `code-mode-round` 浏览器 e2e（录制的真实轮次、无密钥回放），共同锁定整个界面；jsdom 与运行时测试套件则锁定 slot 分发、错误状态、递归详情解析、历史投影与引用稳定的路径复制。
