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
| Owner | Owns | Explicitly does not own |
|---|---|---|
| Client Runtime Conversation engine | Context identity, Location, history replay, view Node publication | Tool event meaning, call tree, Tool renderer |
ui-conversation Tool Definition | call/result pairing, Code Dispatch topology, running/settled/interrupted ToolCallBlock, Chat ordering anchor | Tool-name dispatch, card models, recursive React structure |
ui-conversation Chat view | keyed Node order, scroll anchors, selection, and host actions | Tool lifecycle, subcall composition, atomic Tool renderers |
ui-tool | root/subcall recursive rendering, atomic keyed dispatch, fallback, card models, and details output | Session Event fold, Chat ordering |
| Business Tool plugin | atomic renderers for one or more wire Tool names | root/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 工具 Definition | call/result 配对、Code Dispatch 拓扑、running/settled/interrupted ToolCallBlock、Chat 排序 anchor | 工具名称分发、card model、递归 React 结构 |
ui-conversation Chat view | keyed Node 顺序、scroll anchor、selection 与宿主动作 | 工具 lifecycle、subcall 组合、原子工具 renderer |
ui-tool | root/subcall 递归渲染、原子 keyed dispatch、fallback、card model 与 details output | 会话事件 fold、Chat 排序 |
| 业务工具插件 | 一个或多个 wire 工具名称的原子 renderer | root/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 移动,而不会改变本记录规定的展示所有权。