Merge branch 'master' into web-e2e-interactions
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-26-code-mode-chat-subcall-rows.md: 7d666f0a9e4b8bdb9bd6f5d0d0984fee0c4b21e2
|
||||
2026-07-26-code-mode-chat-subcall-rows.zh.md: fb9b0c62bb702cfdb7ba3c8ccce73d8e43f29c1b
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: Code Mode chat rendering — sub-calls as native rows under the parent
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-26-code-mode-chat-subcall-rows.zh.md)
|
||||
|
||||
> Scope: how the web chat view renders a `run_code` turn — the client-side half of the Code Mode UI stack, built on the [host foundation](2026-07-26-code-dispatch-ui-foundation.md) (full-content `tool/code-dispatch`, the required `description` parameter). The [toolview dissolution](../architecture/2026-07-23-toolview-dissolution.md) owns the slot model this rides on.
|
||||
|
||||
## Problem
|
||||
|
||||
With Code Mode enabled, the chat view showed one opaque `run_code` row: raw program text as the summary, sub-calls invisible everywhere. The settled product requirement is the opposite: each sub-call must render *identically* to a native tool call — same row components, same custom registrations, same details panel — while the transcript stays honest about the fact that the model made ONE call.
|
||||
|
||||
## Decision
|
||||
|
||||
**Sub-calls are `ToolResultNode`s indexed off the surface flow, rendered through the same keyed slot as native rows, nested always-visible under their parent.**
|
||||
|
||||
- **Data layer**: `Session.applyEventSideEffects` folds each in-window `tool/code-dispatch` into `ConversationSnapshot.codeDispatches: ReadonlyMap<parentCallId, readonly CodeSubCall[]>`, where `CodeSubCall` IS `ToolResultNode` (the sub-call id as `callId`, the logged args JSON-stringified into `call.argsRaw`, the full logged `content`/`isError`). Live mux frames and history replay build the identical index (`rebuildDerivedFromWindow` clears and re-derives; copy-on-write per-parent arrays keep snapshot references memo-stable). Sub-calls never join `nodes` — the surface flow remains exactly the model-visible turn structure. The event is narrowed structurally at the wire-consumer boundary (dsh-tools' host types cannot enter the client program — the host/client `Context` merges collide), the same posture as every cross-wire payload.
|
||||
- **Render layer**: `ChatView`'s `CallRow` renders the parent, then — for parents present in the index — a `[data-subcalls]` nest of `SubCallRow`s, each dispatching through the SAME `'conversation.chat.toolview'` keyed hole with `entryKey = sub-tool name` and the same `GenericToolCard` fallback. Identity with native rows holds by construction: a keyed registration (e.g. the bash sample) takes over sub-rows exactly as it takes over top-level rows, with zero registration changes. Running parents (`runningCalls`) nest their so-far dispatches the same way, so sub-rows stream in live during the run (PR1 logs each dispatch as it completes).
|
||||
- **`run_code` presentation**: a new `code` row variant (classifier `run_code → code`, `Code` title, `IconCodeOutline16`) summarizes with the model-authored `description` and expands to the program itself (monospace on the markdown code-block fill) rather than the args JSON envelope.
|
||||
- **Details panel**: `materialFor` falls through nodes → runningCalls → the dispatch index, so a selected sub-callId resolves to full args and complete output through the identical rendering path as a native settled call.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Sub-calls flat in the surface flow (fold them into `nodes`).** Rejected: misrepresents the transcript — the model made one call; nesting under the parent preserves the code↔calls association and keeps the fold's model-visible-order invariant untouched.
|
||||
|
||||
**Hidden until the parent row expands.** Rejected by product decision: the sub-calls ARE the story of a Code Mode turn; hiding them re-creates the opacity this feature removes. The parent's expand toggle reveals only the program.
|
||||
|
||||
**A dedicated sub-call row component.** Rejected: the whole point is identity with native rows; a parallel component would drift. The nest wrapper (indent + left edge) is the only sub-call-specific chrome.
|
||||
|
||||
## Consequences
|
||||
|
||||
Custom toolview registrations apply to sub-calls for free — and deliberately: there is no per-registration opt-out short of the component reading its own context, which no current consumer needs. Selection highlighting reaches nested rows through the same `selectedCallId` channel (group membership tests both levels). Trajectory/waterfall still render `run_code` as a single row — their sub-call spans are deferred to the PR that adds dispatch timing (start/end events), without which a waterfall span would be a lie. Fixture turn 64 (`?fixture`) plus the `code-mode-round` browser e2e (recorded real round, keyless replay) pin the full surface; the jsdom suites pin the slot dispatch, error states, details resolution, and index reference stability.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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)链的 client 侧一半,构建在[宿主侧基础](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` 行:摘要就是原始程序文本,子调用则处处不可见。已敲定的产品要求恰恰相反:每个子调用都必须与原生工具调用渲染得*完全一致*——同样的行组件、同样的自定义注册、同样的 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 载荷一致。
|
||||
- **渲染层**:`ChatView` 的 `CallRow` 先渲染父行,随后对索引中出现的父级渲染一组 `[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 解析与索引引用稳定性。
|
||||
Reference in New Issue
Block a user