refactor(client): localize nested Tool call trees
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md
|
||||
2026-08-08-client-tool-presentation-ownership.md: e61c2030457cc9f0fda214e896b76afb37d0d2bc
|
||||
2026-08-08-client-tool-presentation-ownership.zh.md: 5c56b8c17ef5ca6695f3b28f6b93218dade356c7
|
||||
2026-08-08-client-tool-presentation-ownership.md: 9ea7d7dfc5e18e541550569795c5e1db84d6ad91
|
||||
2026-08-08-client-tool-presentation-ownership.zh.md: 11a42ad69e4374afa6dccd3af74b8bd1b5e5c6f1
|
||||
|
||||
@@ -12,19 +12,19 @@ That ownership made `ui-conversation` interpret business Tool names and made sub
|
||||
|
||||
## Decision
|
||||
|
||||
Tool is a first-class Client UI concept with one presentation owner, `@deepseek-ai/dsh-client-ui-tool`. Session Event, projection, fold, `ConversationSnapshot` construction and caching, historical paging, and Code Dispatch indexing remain unchanged.
|
||||
Tool is a first-class Client UI concept with one presentation owner, `@deepseek-ai/dsh-client-ui-tool`. Runtime normalizes Code Dispatch into recursive `ToolCallBlock` values: every root or child owns its next level through `subCalls`, and `ConversationSnapshot` exposes no separate parent-to-children map.
|
||||
|
||||
“First-class concept” describes UI ownership only; it adds no Runtime data kind. `ConversationNode` remains the transcript projection, `ChatFlowItem` remains the render unit produced when conversation sorts and groups nodes, `ToolCallBlock` remains the standard data for one call, and `ToolCallTree` only composes root/subcall presentation within Tool. Command continues to render through the separate `'conversation.chat.commandview'` seat and does not become Tool.
|
||||
|
||||
`ui-conversation` owns ordered placement. `deriveChatFlow()` still decides where a settled Tool group appears, and `ChatView` still appends running calls, maintains scroll anchors and selection, and supplies host actions. For each root call it renders the single/session `'conversation.chat.tool'` seat with the root block, selected call id, session cwd, and open-file/inspect callbacks. It does not read Code Dispatch children, branch on Tool names, or import Tool-specific views and card models.
|
||||
|
||||
`ui-tool` occupies that whole-Tool seat. Through its standard session slot props, `ToolCallTree` selects the Runtime-projected `codeDispatches[rootCallId]` array, renders the root followed by that one currently supported child level, and routes both forms through one keyed/session `'tool.call.toolview'` child slot using `entryKey: toolName`. An absent business registration renders `GenericToolCard`. This is deliberately one-level composition, not a claim that the Runtime supports an arbitrary recursive call graph.
|
||||
`ui-tool` occupies that whole-Tool seat. `ToolCallTree` recursively walks the root block's `subCalls` and routes every level through one keyed/session `'tool.call.toolview'` child slot using `entryKey: toolName`. An absent business registration renders `GenericToolCard`. It neither reads Session nor maintains a second call topology.
|
||||
|
||||
Business plugins register only atomic views against `'tool.call.toolview'`. Their owner payload is the standard Tool call block plus identity, cwd, and host actions; it carries no Session projector or conversation service. Skill remains an ordinary Tool and `ui-skill` registers the `skill` key through this seam. Existing first-party views live in `ui-tool` until a business package has a reason to own one independently.
|
||||
|
||||
The details panel is a second Tool presentation site but not a call-tree owner. `ui-conversation` delegates its selected output body through the single/session `'conversation.details.tool'` seat; `ui-tool` renders the card-aware output and the seat fallback preserves raw result text when the plugin is absent. Card models therefore have one production owner without introducing a reverse implementation import.
|
||||
|
||||
The Runtime remains the authority for Tool lifecycle and call topology. Code Dispatch stays a top-level official concept because it changes `codeDispatches` and parent/child identity; ordinary Tool business differences stay at the keyed presentation seam. This package boundary does not add a Tool projector/fold registry.
|
||||
The Runtime remains the authority for Tool lifecycle and call topology. Code Dispatch is an official top-level concept because it changes parent/child identity; a private `ToolCallTree` shares one fold between live and history paths and projects its index into standard recursive call blocks. Ordinary Tool business differences stay at the keyed presentation seam, and this package boundary adds no Tool projector/fold registry.
|
||||
|
||||
## Runtime and render path
|
||||
|
||||
@@ -39,22 +39,22 @@ ConversationSnapshot.runningCalls |
|
||||
-> ChatView flow tail ---------------+-> ToolSeat
|
||||
-> conversation.chat.tool
|
||||
-> ToolCallTree
|
||||
ConversationSnapshot.codeDispatches[rootCallId] -+
|
||||
+-> root ToolCall + one-level child ToolCall
|
||||
-> tool.call.toolview(entryKey = toolName)
|
||||
|- registered atomic view
|
||||
`- GenericToolCard fallback
|
||||
-> root ToolCallBlock
|
||||
`- subCalls[] (recursive)
|
||||
-> tool.call.toolview(entryKey = toolName)
|
||||
|- registered atomic view
|
||||
`- GenericToolCard fallback
|
||||
```
|
||||
|
||||
The live Session's [`Session.buildSnapshot()`](../../../../packages/client/runtime/src/client/sessions/session.ts) caches arrays or maps such as `nodes`, `runningCalls`, and `codeDispatches` against independent revisions. Their references stay stable when the corresponding business state has not changed, allowing React selectors and memoization to skip unrelated updates. The historical projection's [`projectConversationHistory()`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) reconstructs the same running-call and Code Dispatch shapes from entries in its window. Tool UI consumes the snapshot shapes already unified by those paths; presentation packages do not repeat call/result pairing, historical replay, or cache indexing.
|
||||
Runtime's [`ToolCallTree`](../../../../packages/client/runtime/src/client/sessions/tool-call-tree.ts) privately indexes child lifecycles by parent callId and is shared by the live [`Session.buildSnapshot()`](../../../../packages/client/runtime/src/client/sessions/session.ts) and historical [`projectConversationHistory()`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) paths. It recursively projects children onto root `ToolCallBlock` values and copies only the owning ancestor path when a child changes. Unchanged siblings, other roots, and snapshot references with no Tool-topology change stay stable so React selectors and memoization can skip unrelated updates. Tool UI consumes this unified tree without repeating call/result pairing, historical replay, or cache indexing.
|
||||
|
||||
[`ChatView`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx) reruns [`deriveChatFlow()`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts) only when the `nodes` reference changes. It groups consecutive settled Tool results into a `tool-group`, while running root calls append at the flow tail. Both paths ultimately enter the same `ToolSeat`, so settled and running forms share the whole-Tool seat. `ToolCallTree` selects only the current root's `codeDispatches[rootCallId]`; it does not introduce a business projector for presentation of other roots.
|
||||
[`ChatView`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx) reruns [`deriveChatFlow()`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts) only when the `nodes` reference changes. It groups consecutive settled Tool results into a `tool-group`, while running root calls append at the flow tail. Both paths ultimately enter the same `ToolSeat`, so settled and running forms share the whole-Tool seat. Selection is passed only to the root containing that call, and `ToolCallTree` then renders recursively within that local tree.
|
||||
|
||||
## Code and responsibility boundaries
|
||||
|
||||
| Owner | Primary code | Owns | Explicitly does not own |
|
||||
|---|---|---|---|
|
||||
| Client Runtime | [`Session`](../../../../packages/client/runtime/src/client/sessions/session.ts), [`history-fold.ts`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) | call/result pairing, running/settled lifecycle, Code Dispatch parent/child index, snapshot reference stability | Business views selected by Tool name |
|
||||
| Client Runtime | [`Session`](../../../../packages/client/runtime/src/client/sessions/session.ts), [`ToolCallTree`](../../../../packages/client/runtime/src/client/sessions/tool-call-tree.ts), [`history-fold.ts`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) | call/result pairing, running/settled lifecycle, recursive parent/child tree, snapshot structural sharing | Business views selected by Tool name |
|
||||
| `ui-conversation` | [`chat-flow.ts`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts), [`ChatView.tsx`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx), [`slots.ts`](../../../../packages/client/ui-conversation/src/client/contract/slots.ts) | ChatFlow order, settled groups, running tail, scroll anchors, selection and host actions, whole-Tool seat declaration | subcall composition, `toolName` dispatch, Generic fallback, Tool card models |
|
||||
| `ui-tool` | [`apply.ts`](../../../../packages/client/ui-tool/src/client/apply.ts), [`ToolCallTree.tsx`](../../../../packages/client/ui-tool/src/client/tool/ToolCallTree.tsx), [`slots.ts`](../../../../packages/client/ui-tool/src/client/contract/slots.ts) | root/subcall composition, atomic keyed dispatch, Generic fallback, Tool card models and built-in Tool views | ChatFlow ordering, Session Event fold |
|
||||
| Business Tool plugins | [`ui-skill` registration example](../../../../packages/client/ui-skill/src/client/index.ts) | Atomic views for one or more wire Tool names | root/subcall placement and lifecycle pairing |
|
||||
@@ -82,7 +82,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
|
||||
## Details path
|
||||
|
||||
[`DetailsPanel`](../../../../packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx) still locates the selected call in `nodes`, `runningCalls`, and `codeDispatches`, and it owns input arguments, empty states, and panel lifecycle. It passes only `{ block, cwd }` to `'conversation.details.tool'`; [`ToolDetails`](../../../../packages/client/ui-tool/src/client/tool/ToolDetails.tsx) reuses Tool card models to render the output. When `ui-tool` is absent, a settled call falls back to raw result text and a running call shows conversation's running fallback, so details never imports the Tool implementation in reverse.
|
||||
[`DetailsPanel`](../../../../packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx) locates the selected call recursively in `nodes` and `runningCalls` through their `subCalls`, and it owns input arguments, empty states, and panel lifecycle. It passes only `{ block, cwd }` to `'conversation.details.tool'`; [`ToolDetails`](../../../../packages/client/ui-tool/src/client/tool/ToolDetails.tsx) reuses Tool card models to render the output. When `ui-tool` is absent, a settled call falls back to raw result text and a running call shows conversation's running fallback, so details never imports the Tool implementation in reverse.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -96,7 +96,7 @@ Test ownership follows production ownership. `ui-conversation` tests install a l
|
||||
|
||||
**Add business-specific Session projectors or folds.** Rejected: ordinary Tool views consume the standard call block already reconstructed by Runtime. A second registry would create two authorities for call identity and historical replay. Only a feature that changes logged topology or lifecycle earns a Runtime-level extension.
|
||||
|
||||
**Make each atomic Tool view render its own subcalls recursively.** Rejected: the atomic registrant receives one Tool call and should not know whether it is a root or child. Root/child composition belongs to `ui-tool`, and the current wire/runtime shape only supports one Code Dispatch child level.
|
||||
**Make each atomic Tool view render its own subcalls recursively.** Rejected: the atomic registrant receives one Tool call and should not know whether it is a root or child. Recursive root/child composition belongs centrally to `ui-tool`'s `ToolCallTree`.
|
||||
|
||||
**Import `ui-tool` components directly from `ui-conversation`.** Rejected: it would reverse the intended feature direction and make Tool presentation mandatory. Declared slots retain lifecycle ownership, fallback behavior, and independent plugin loading.
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ Client Runtime 已经把 Tool 调用投影成稳定的生命周期:它按 `cal
|
||||
|
||||
## Decision
|
||||
|
||||
Tool 成为 Client UI 的一级概念,并由 `@deepseek-ai/dsh-client-ui-tool` 统一拥有展示。Session Event、projection、fold、`ConversationSnapshot` 构建与缓存、历史分页及 Code Dispatch 索引保持不变。
|
||||
Tool 成为 Client UI 的一级概念,并由 `@deepseek-ai/dsh-client-ui-tool` 统一拥有展示。Runtime 将 Code Dispatch 规范化为递归 `ToolCallBlock`:每个 root 或 child 通过自己的 `subCalls` 拥有下一层调用,`ConversationSnapshot` 不再公开单独的 parent-to-children map。
|
||||
|
||||
这里的“一级概念”只描述 UI 所有权,不增加 Runtime 数据种类。`ConversationNode` 仍是 transcript projection,`ChatFlowItem` 仍是 conversation 对节点进行排序与分组后得到的渲染单元,`ToolCallBlock` 仍是单次调用的标准数据,而 `ToolCallTree` 只负责 Tool 内部的 root/subcall 展示编排。Command 继续通过独立的 `'conversation.chat.commandview'` 席位渲染,不并入 Tool。
|
||||
|
||||
`ui-conversation` 拥有有序放置。`deriveChatFlow()` 仍决定 settled Tool group 在哪里出现,`ChatView` 仍追加 running call、维护滚动 anchor 与 selection,并提供宿主动作。对于每个 root call,它使用 root block、selected call id、session cwd 以及 open-file/inspect 回调渲染 single/session 的 `'conversation.chat.tool'` 席位。它不读取 Code Dispatch child、不按 Tool 名称分支,也不导入 Tool 专属 view 或 card model。
|
||||
|
||||
`ui-tool` 占据这个整体 Tool 席位。`ToolCallTree` 通过标准 session slot props 选择 Runtime 投影的 `codeDispatches[rootCallId]` 数组,先渲染 root,再渲染当前支持的一层 child;两种调用都通过同一个 keyed/session 的 `'tool.call.toolview'` 子 slot,以 `entryKey: toolName` 分发。业务未注册时渲染 `GenericToolCard`。这里刻意只编排一层,并不声称 Runtime 已支持任意递归调用图。
|
||||
`ui-tool` 占据这个整体 Tool 席位。`ToolCallTree` 直接递归遍历 root block 的 `subCalls`,并让每一层调用都通过同一个 keyed/session 的 `'tool.call.toolview'` 子 slot,以 `entryKey: toolName` 分发。业务未注册时渲染 `GenericToolCard`。它不读取 Session,也不维护第二份调用拓扑。
|
||||
|
||||
业务插件只对 `'tool.call.toolview'` 注册原子 view。其 owner payload 是标准 Tool call block 加 identity、cwd 与宿主动作,不携带 Session projector 或 conversation service。Skill 仍是普通 Tool,`ui-skill` 通过该 seam 注册 `skill` key。现有第一方 view 暂留在 `ui-tool`,直到某个业务包确有理由独立拥有它。
|
||||
|
||||
details panel 是第二个 Tool 展示点,但不是调用树所有者。`ui-conversation` 通过 single/session 的 `'conversation.details.tool'` 席位委托 selected output body;`ui-tool` 渲染能够识别 card 的输出,插件缺席时由席位 fallback 保留 raw result text。因此 card model 只有一个生产代码所有者,也不需要引入反向实现依赖。
|
||||
|
||||
Runtime 仍是 Tool 生命周期与调用拓扑的权威。Code Dispatch 会改变 `codeDispatches` 与 parent/child identity,因此继续作为官方顶级概念;普通 Tool 业务差异停留在 keyed 展示 seam。这个包边界不会增加 Tool projector/fold registry。
|
||||
Runtime 仍是 Tool 生命周期与调用拓扑的权威。Code Dispatch 作为官方顶级概念改变 parent/child identity;私有 `ToolCallTree` 对 live 与 history 共用同一套 fold,并把索引投影成标准递归 call block。普通 Tool 业务差异停留在 keyed 展示 seam,这个包边界不会增加 Tool projector/fold registry。
|
||||
|
||||
## Runtime 与渲染链路
|
||||
|
||||
@@ -39,22 +39,22 @@ ConversationSnapshot.runningCalls |
|
||||
-> ChatView flow tail ---------------+-> ToolSeat
|
||||
-> conversation.chat.tool
|
||||
-> ToolCallTree
|
||||
ConversationSnapshot.codeDispatches[rootCallId] -+
|
||||
+-> root ToolCall + one-level child ToolCall
|
||||
-> tool.call.toolview(entryKey = toolName)
|
||||
|- registered atomic view
|
||||
`- GenericToolCard fallback
|
||||
-> root ToolCallBlock
|
||||
`- subCalls[] (recursive)
|
||||
-> tool.call.toolview(entryKey = toolName)
|
||||
|- registered atomic view
|
||||
`- GenericToolCard fallback
|
||||
```
|
||||
|
||||
Live Session 的 [`Session.buildSnapshot()`](../../../../packages/client/runtime/src/client/sessions/session.ts) 按独立 revision 缓存 `nodes`、`runningCalls`、`codeDispatches` 等数组或 map;没有对应业务变化时,它们保持引用稳定,供 React selector 与 memo 跳过无关更新。历史 projection 的 [`projectConversationHistory()`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) 从窗口内 entry 重建相同的 running call 与 Code Dispatch 形态。Tool UI 直接消费这两个路径已经统一的 snapshot,不在展示包中重复 call/result 配对、历史 replay 或缓存索引。
|
||||
Runtime 的 [`ToolCallTree`](../../../../packages/client/runtime/src/client/sessions/tool-call-tree.ts) 私下按 parent callId 索引 child lifecycle,并供 Live [`Session.buildSnapshot()`](../../../../packages/client/runtime/src/client/sessions/session.ts) 与历史 [`projectConversationHistory()`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) 共用。它把 children 递归投影到 root `ToolCallBlock`,child 变化时只复制所属祖先路径;未变化的 sibling、其他 root,以及没有 Tool 拓扑变化的 snapshot 引用保持稳定,供 React selector 与 memo 跳过无关更新。Tool UI 直接消费这两个路径统一后的树,不重复 call/result 配对、历史 replay 或缓存索引。
|
||||
|
||||
[`ChatView`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx) 只在 `nodes` 引用变化时重新执行 [`deriveChatFlow()`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts),把连续 settled Tool result 合为 `tool-group`;running root call 则追加在 flow tail。两条路径最终都进入同一个 `ToolSeat`,因此 settled/running 形态共享整体 Tool 席位。`ToolCallTree` 只选择当前 root 的 `codeDispatches[rootCallId]`,不会因其他 root 的展示逻辑引入业务 projector。
|
||||
[`ChatView`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx) 只在 `nodes` 引用变化时重新执行 [`deriveChatFlow()`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts),把连续 settled Tool result 合为 `tool-group`;running root call 则追加在 flow tail。两条路径最终都进入同一个 `ToolSeat`,因此 settled/running 形态共享整体 Tool 席位。selection 只传给包含该 call 的 root,`ToolCallTree` 再沿该 root 的局部树递归渲染。
|
||||
|
||||
## 代码与职责边界
|
||||
|
||||
| 所有者 | 主要代码 | 拥有的责任 | 明确不拥有 |
|
||||
|---|---|---|---|
|
||||
| Client Runtime | [`Session`](../../../../packages/client/runtime/src/client/sessions/session.ts)、[`history-fold.ts`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) | call/result 配对、running/settled 生命周期、Code Dispatch parent/child 索引、snapshot 引用稳定性 | Tool 名称对应的业务视图 |
|
||||
| Client Runtime | [`Session`](../../../../packages/client/runtime/src/client/sessions/session.ts)、[`ToolCallTree`](../../../../packages/client/runtime/src/client/sessions/tool-call-tree.ts)、[`history-fold.ts`](../../../../packages/client/runtime/src/client/session-history/history-fold.ts) | call/result 配对、running/settled 生命周期、递归 parent/child 树、snapshot 结构共享 | Tool 名称对应的业务视图 |
|
||||
| `ui-conversation` | [`chat-flow.ts`](../../../../packages/client/ui-conversation/src/client/chat/chat-flow.ts)、[`ChatView.tsx`](../../../../packages/client/ui-conversation/src/client/chat/ChatView.tsx)、[`slots.ts`](../../../../packages/client/ui-conversation/src/client/contract/slots.ts) | ChatFlow 顺序、settled group、running tail、scroll anchor、selection 与宿主动作、整体 Tool 席位声明 | subcall 组合、按 `toolName` 分发、Generic fallback、Tool card model |
|
||||
| `ui-tool` | [`apply.ts`](../../../../packages/client/ui-tool/src/client/apply.ts)、[`ToolCallTree.tsx`](../../../../packages/client/ui-tool/src/client/tool/ToolCallTree.tsx)、[`slots.ts`](../../../../packages/client/ui-tool/src/client/contract/slots.ts) | root/subcall 组合、原子 keyed dispatch、Generic fallback、Tool card model 与内置 Tool view | ChatFlow 排序、Session Event fold |
|
||||
| 业务 Tool 插件 | [`ui-skill` 注册例](../../../../packages/client/ui-skill/src/client/index.ts) | 一个或多个 wire Tool name 的原子 view | root/subcall 位置与生命周期配对 |
|
||||
@@ -82,7 +82,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
|
||||
## Details 路径
|
||||
|
||||
[`DetailsPanel`](../../../../packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx) 仍从 `nodes`、`runningCalls` 与 `codeDispatches` 中定位选中的 call,并拥有 input 参数、空态和面板生命周期。它只把 `{ block, cwd }` 交给 `'conversation.details.tool'`;[`ToolDetails`](../../../../packages/client/ui-tool/src/client/tool/ToolDetails.tsx) 复用 Tool card model 渲染 output。`ui-tool` 缺席时,settled call 回退为 raw result text,running call 显示 conversation 的 running fallback,因此 details 不反向导入 Tool 实现。
|
||||
[`DetailsPanel`](../../../../packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx) 在 `nodes` 与 `runningCalls` 的递归 `subCalls` 中定位 selected call,并拥有 input 参数、空态和面板生命周期。它只把 `{ block, cwd }` 交给 `'conversation.details.tool'`;[`ToolDetails`](../../../../packages/client/ui-tool/src/client/tool/ToolDetails.tsx) 复用 Tool card model 渲染 output。`ui-tool` 缺席时,settled call 回退为 raw result text,running call 显示 conversation 的 running fallback,因此 details 不反向导入 Tool 实现。
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -96,7 +96,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
|
||||
**增加业务专属 Session projector 或 fold。** 拒绝:普通 Tool view 消费 Runtime 已重建的标准 call block。第二套 registry 会为 call identity 与历史 replay 建立两个权威。只有会改变日志拓扑或生命周期的能力才应获得 Runtime 级扩展。
|
||||
|
||||
**让每个原子 Tool view 递归渲染自己的 subcall。** 拒绝:原子注册方只接收一个 Tool call,不应知道自己是 root 还是 child。root/child 编排归 `ui-tool`,且当前 wire/runtime 形态只支持一层 Code Dispatch child。
|
||||
**让每个原子 Tool view 递归渲染自己的 subcall。** 拒绝:原子注册方只接收一个 Tool call,不应知道自己是 root 还是 child。递归 root/child 编排统一归 `ui-tool` 的 `ToolCallTree`。
|
||||
|
||||
**让 `ui-conversation` 直接导入 `ui-tool` 组件。** 拒绝:这会反转预期的 feature 依赖方向,并把 Tool 展示变成必选能力。声明式 slot 能保留生命周期所有权、fallback 行为与独立插件装载。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user