DSH / Atlas
2026-08-08implementedarchitecture

Client Tool presentation ownership

Client 工具展示所有权

Client Runtime already paired Tool call/result events by `callId` and could recover root/subcall topology from Code Dispatch events, but the Chat view also owned Tool placement in the conversation flow, recursive call-tree composition, Tool-name dispatch, the Generic fallback, card models, and first-party Tool renderers. `ui-conversation` therefore had to interpret every business Tool name; moving individual React co

English

Problem

Client Runtime already paired Tool call/result events by callId and could recover root/subcall topology from Code Dispatch events, but the Chat view also owned Tool placement in the conversation flow, recursive call-tree composition, Tool-name dispatch, the Generic fallback, card models, and first-party Tool renderers. ui-conversation therefore had to interpret every business Tool name; moving individual React components did not change that ownership, and removing atomic renderers left subcalls without a presentation owner.

Tool presentation needed an independent owner without adding a second registry beside Client slots or making every atomic Tool renderer understand root/subcall structure.

Decision

Tool is a first-class Client UI presentation concept. @deepseek-ai/dsh-client-ui-tool owns root/subcall composition, atomic renderer dispatch by wire Tool name, the Generic fallback, card models, and details output. Business plugins register only their atomic Tool renderers and do not modify conversation or Session.

Conversation data assembly follows the later Conversation business-node decision. The ui-conversation Tool Definition pairs root call/result Session Events, folds Code Dispatch edges into recursive ToolCallBlock.subCalls, and emits one stable tool-call Chat Node. This data responsibility handles only official Tool identity and topology; it does not interpret presentation for concrete Tool names.

ChatView only places generic ChatNodeSeat entries in Chat snapshot order. A Seat dispatches 'conversation.chat.node' by node.kind; ui-tool registers the tool-call entry, and ToolCallTree recursively traverses the root block. Every root or child level dispatches through the same keyed/session 'tool.call.toolview' child slot with entryKey: toolName, falling back to GenericToolCard when no registration exists.

A business Tool plugin receives one standard ToolCallBlock, identity, workspace cwd, and host actions; it does not read Session, Context, or the Conversation assembler. Skill remains an ordinary Tool and uses the same keyed-slot registration path as other business Tools.

The details panel is a second Tool presentation point, not the call-tree owner. ui-conversation locates the selected call and delegates its output body through 'conversation.details.tool'; ui-tool reuses the card model, while the conversation fallback retains raw result text when the plugin is absent.

Runtime and render path

Session Event window
  -> Tool Definition -> tool-call Chat Node (recursive ToolCallBlock)
  -> ChatView -> ChatNodeSeat(entryKey = tool-call)
  -> ToolCallTree
       -> root/subCalls[] recursion
       -> tool.call.toolview(entryKey = toolName)
            |- registered atomic view
            `- GenericToolCard fallback

Ownership boundary

OwnerOwnsExplicitly does not own
Client Runtime Conversation engineContext identity, Location, history replay, view Node publicationTool event meaning, call tree, Tool renderer
ui-conversation Tool Definitioncall/result pairing, Code Dispatch topology, running/settled/interrupted ToolCallBlock, Chat ordering anchorTool-name dispatch, card models, recursive React structure
ui-conversation Chat viewkeyed Node order, scroll anchors, selection, and host actionsTool lifecycle, subcall composition, atomic Tool renderers
ui-toolroot/subcall recursive rendering, atomic keyed dispatch, fallback, card models, and details outputSession Event fold, Chat ordering
Business Tool pluginatomic renderers for one or more wire Tool namesroot/subcall placement, lifecycle pairing, Session projectors

Verification

ui-conversation tests pin the Tool Definition's call/result pairing, Code Dispatch, interruption, and running-to-settled keyed identity without importing production ui-tool renderers. ui-tool tests mount the real conversation host and pin root/subcall recursion, keyed dispatch, Generic fallback, selection, details, and concrete Tool cards. Assembled Web tests cover the path with both plugins loaded.

Alternatives considered

Keep atomic Tool slots under every conversation view. Rejected: every view would repeat root/subcall composition and Tool registration would split by view. The whole Tool renderer occupies one business Node slot in a view, while Tool owns atomic dispatch.

Move only Tool React components and card models. Rejected: conversation would still dispatch by Tool name and recurse through subcalls, so file movement would not create an ownership boundary.

Create a Tool-specific projector/fold registry. Rejected: the general Conversation assembler already owns Context identity, history windows, and publication. A second Runtime registry would create two lifecycle authorities.

Let every atomic Tool renderer recurse through its subcalls. Rejected: an atomic registrant should understand one Tool call without knowing whether it is a root or child. ToolCallTree handles recursive structure once.

Let ui-conversation import ui-tool components directly. Rejected: this would reverse the feature dependency and make Tool presentation mandatory. Slots preserve independent loading, lifecycle, and fallback behavior.

Consequences

ui-conversation no longer depends on presentation for concrete Tool names, and root and subcalls cannot drift onto different dispatch paths. Business packages can independently own atomic Tool renderers; if ui-tool is absent, Conversation data assembly remains valid, Chat Nodes use the generic fallback, and details retain raw results.

The cost is an explicit dependency from ui-tool on the business Node slot and locale namespace declared by conversation, plus one Tool-specific child slot. Tool Definition remains in ui-conversation because this change does not split packages; it can later move through the Conversation registry seam without changing the presentation ownership recorded here.

中文

问题

Client 运行时已经按 callId 配对工具调用/结果事件,并能从 Code Dispatch 事件恢复 root/subcall 拓扑,但 Chat view 曾同时拥有工具在对话流中的放置、递归调用树编排、按工具名称分发、Generic fallback、card model 和第一方工具 renderer。ui-conversation 因此必须解释每个业务工具名称;只移动单个 React 组件不会改变这层所有权,移走原子 renderer 后 subcall 的展示也会无人负责。

工具展示需要一个独立所有者,同时不能建立与 Client slot 平行的第二套注册表,也不能让每个原子工具 renderer 自己理解 root/subcall 结构。

决策

工具是 Client UI 的一级展示概念,由 @deepseek-ai/dsh-client-ui-tool 统一拥有 root/subcall 编排、按 wire 工具名称的原子 renderer 分发、Generic fallback、card model 和 details output。业务插件只注册自己的原子工具 renderer,不修改 conversation 或会话。

Conversation 数据组装遵循后续的 Conversation 业务节点决策ui-conversation 的工具 Definition 从会话事件配对 root call/result,把 Code Dispatch edge fold 成递归 ToolCallBlock.subCalls,并生成一个稳定的 tool-call Chat Node;这里的数据职责只处理官方工具 identity 和拓扑,不解释具体工具名称的展示。

ChatView 只按 Chat 快照的 order 放置通用 ChatNodeSeat。Seat 以 node.kind 分发 'conversation.chat.node'ui-tool 注册 tool-call entry,并由 ToolCallTree 递归遍历 root block。每一层 root 或 child 都通过同一个 keyed/session 'tool.call.toolview' 子 slot 以 entryKey: toolName 分发,缺少注册时渲染 GenericToolCard

业务工具插件接收一个标准 ToolCallBlock、identity、workspace cwd 和宿主动作,不读取会话、上下文或 Conversation assembler。skill(技能)仍是普通工具;它和其他业务工具使用同一 keyed slot 注册路径。

details panel 是第二个工具展示点,但不是调用树所有者。ui-conversation 定位 selected call,并通过 'conversation.details.tool' 委托 output body;ui-tool 复用 card model,插件缺席时 conversation fallback 保留 raw result text。

运行时与渲染路径

Session Event window
  -> Tool Definition -> tool-call Chat Node (recursive ToolCallBlock)
  -> ChatView -> ChatNodeSeat(entryKey = tool-call)
  -> ToolCallTree
       -> root/subCalls[] recursion
       -> tool.call.toolview(entryKey = toolName)
            |- registered atomic view
            `- GenericToolCard fallback

所有权边界

所有者拥有明确不拥有
Client 运行时 Conversation engine上下文 identity、Location、历史回放、view Node 发布工具事件含义、调用树、工具 renderer
ui-conversation 工具 Definitioncall/result 配对、Code Dispatch 拓扑、running/settled/interrupted ToolCallBlock、Chat 排序 anchor工具名称分发、card model、递归 React 结构
ui-conversation Chat viewkeyed Node 顺序、scroll anchor、selection 与宿主动作工具 lifecycle、subcall 组合、原子工具 renderer
ui-toolroot/subcall 递归渲染、原子 keyed dispatch、fallback、card model 与 details output会话事件 fold、Chat 排序
业务工具插件一个或多个 wire 工具名称的原子 rendererroot/subcall 位置、生命周期配对、会话 projector

验证

ui-conversation 测试固定工具 Definition 的 call/result 配对、Code Dispatch、interruption 和 running-to-settled keyed identity,不导入 ui-tool 的生产 renderer。ui-tool 测试挂载真实 conversation 宿主,固定 root/subcall 递归、keyed dispatch、Generic fallback、selection、details 和具体工具 card。组装后的 Web 测试覆盖两个插件共同装载的路径。

考虑过的替代方案

在每个 conversation view 下保留原子工具 slot。 拒绝:每个 view 都要重复 root/subcall 编排,工具注册也会按 view 分裂。整个工具 renderer 占据 view 的一个业务 Node slot,原子分发由工具自己拥有。

只移动工具 React 组件与 card model。 拒绝:conversation 仍会按工具名称分发并递归 subcall,文件位置变化不产生所有权边界。

为工具建立专属 projector/fold 注册表。 拒绝:通用 Conversation assembler 已拥有上下文 identity、历史窗口和发布;第二个运行时注册表会制造生命周期的双重权威。

让每个原子工具 renderer 递归自己的 subcall。 拒绝:原子注册方只应理解一个工具调用,不应知道自己是 root 还是 child。递归结构统一由 ToolCallTree 处理。

ui-conversation 直接导入 ui-tool 组件。 拒绝:这会反转功能依赖并把工具展示变成必选能力。slot 保留独立装载、生命周期和 fallback。

后果

ui-conversation 不再依赖工具名称对应的业务展示,root 与 subcall 也不会漂移到不同分发路径。业务包可以独立拥有原子工具 renderer;ui-tool 缺席时,Conversation 数据组装仍然成立,Chat Node 使用通用 fallback,details 保留 raw result。

代价是 ui-tool 明确依赖 conversation 声明的业务 Node slot 和 locale namespace,并拥有一个工具专属子 slot。工具 Definition 暂时位于 ui-conversation,因为本次没有拆包;它以后可以沿 Conversation 注册表 seam 移动,而不会改变本记录规定的展示所有权。