perf(tool): retain unchanged subcall branches
This commit is contained in:
1 parent
b2817d77fb
commit
5840898e07
5 files changed
+23
-9
No files matched your search
@@ -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 packages/client/ui-tool/README.md
|
||||
README.md: 8f0dfd0bc0222bb6ea428deb0dd6769f7347e41c
|
||||
README.zh.md: bd4681022f1215a3e0dcda48b73c911b5bc6a283
|
||||
README.md: 39b05236deae913d8be31be7846cac6f853b7eca
|
||||
README.zh.md: e06ec350ad299593c82a216949fcdfa713122387
|
||||
@@ -63,7 +63,7 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool
|
||||
|
||||
### Rendering contract
|
||||
|
||||
`ToolCallTree` receives one Tool node, Session `cwd`, and navigation callbacks. It derives explicit phase props from the root and dispatches each tool by name through `tool.call.toolview`. Dispatched roots retain their recursive `subCalls`; preparation has no children. Each root and child wrapper preserves the `data-chat-anchor-key="call:<id>"` and `data-chat-call-id` DOM contract used for paging and selection. The Tool node keeps the same callId across all three stages.
|
||||
`ToolCallTree` receives one Tool node, Session `cwd`, and navigation callbacks. Each branch receives a stable block and memoizes its explicit phase props, so changing one subcall leaves unchanged sibling branches unrendered. It dispatches each tool by name through `tool.call.toolview`. Dispatched roots retain their recursive `subCalls`; preparation has no children. Each root and child wrapper preserves the `data-chat-anchor-key="call:<id>"` and `data-chat-call-id` DOM contract used for paging and selection. The Tool node keeps the same callId across all three stages.
|
||||
|
||||
Tool owner props forward Chat's stable `useDisclosure` Hook through root and nested calls. Rows invoke it where they own their expanded bodies; intermediate renderers do not subscribe. Each invocation has independent open state that resets when the enclosing Turn collapses, without replacing React identity. Presentation-mode switches preserve it.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
|
||||
|
||||
### 渲染约定
|
||||
|
||||
`ToolCallTree` 接收一个 Tool 节点、会话 `cwd` 和导航回调。它从根调用数据生成显式阶段 props,并通过 `tool.call.toolview` 按工具名分发。已派发的根调用保留递归 `subCalls`,准备阶段没有子调用。每个根调用和子调用包装层都保留 `data-chat-anchor-key="call:<id>"` 与 `data-chat-call-id` DOM 约定,供分页和选择使用。Tool 节点在三个阶段保持同一个 callId。
|
||||
`ToolCallTree` 接收一个 Tool 节点、会话 `cwd` 和导航回调。每个分支接收稳定的 block,并缓存显式阶段 props,因此一个子调用变化不会重渲染未变化的兄弟分支。它通过 `tool.call.toolview` 按工具名分发。已派发的根调用保留递归 `subCalls`,准备阶段没有子调用。每个根调用和子调用包装层都保留 `data-chat-anchor-key="call:<id>"` 与 `data-chat-call-id` DOM 约定,供分页和选择使用。Tool 节点在三个阶段保持同一个 callId。
|
||||
|
||||
Tool 所有者属性将 Chat 注入的稳定 `useDisclosure` 钩子传给根调用及嵌套调用。工具行在拥有展开正文的位置调用它,中间 renderer 不订阅。每次调用拥有独立展开状态,外层轮次收起时重置该状态,不替换 React 身份;展示模式切换保留该状态。
|
||||
|
||||
|
||||
@@ -59,11 +59,12 @@ const ToolCall = memo(function ToolCall({
|
||||
})
|
||||
|
||||
const ToolCallBranch = memo(function ToolCallBranch({
|
||||
renderSlot, call, cwd, home, openFile, inspectCall, loadImage, useDisclosure, t,
|
||||
renderSlot, block, cwd, home, openFile, inspectCall, loadImage, useDisclosure, t,
|
||||
}: Pick<ToolTreeProps, 'renderSlot' | 'cwd' | 'openFile' | 'inspectCall' | 'loadImage' | 'useDisclosure' | 't'> & {
|
||||
call: ToolCallPhaseProps
|
||||
block: ToolCallBlock
|
||||
home?: string | undefined
|
||||
}) {
|
||||
const call = useMemo(() => toolCallPhase(block), [block])
|
||||
return (
|
||||
<ToolCall
|
||||
renderSlot={renderSlot}
|
||||
@@ -84,7 +85,7 @@ const ToolCallBranch = memo(function ToolCallBranch({
|
||||
<ToolCallBranch
|
||||
key={child.callId}
|
||||
renderSlot={renderSlot}
|
||||
call={toolCallPhase(child)}
|
||||
block={child}
|
||||
cwd={cwd}
|
||||
home={home}
|
||||
openFile={openFile}
|
||||
@@ -110,11 +111,10 @@ export function ToolCallTree({
|
||||
renderSlot, node, cwd, openFile, inspectCall, loadImage, useDisclosure, useHostInfo, t,
|
||||
}: ToolTreeProps) {
|
||||
const home = useHostInfo(info => info.home)
|
||||
const call = useMemo(() => toolCallPhase(node.data.root), [node.data.root])
|
||||
return (
|
||||
<ToolCallBranch
|
||||
renderSlot={renderSlot}
|
||||
call={call}
|
||||
block={node.data.root}
|
||||
cwd={cwd}
|
||||
home={home}
|
||||
openFile={openFile}
|
||||
|
||||
@@ -61,6 +61,20 @@ function props(
|
||||
}
|
||||
|
||||
describe('ToolCallTree', () => {
|
||||
it('dispatches only the parent and changed child among 100 sibling calls', () => {
|
||||
const children = Array.from({ length: 100 }, (_, index) => root(`child-${index}`, { name: 'custom_child', argsRaw: '{}' }))
|
||||
const block = { ...root('parent', { name: 'custom_parent', argsRaw: '{}' }), subCalls: children }
|
||||
const owners: ToolCallOwnerProps[] = []
|
||||
const initial = props(block, undefined, owners)
|
||||
const view = render(<ToolCallTree {...initial} />)
|
||||
expect(owners).toHaveLength(101)
|
||||
owners.length = 0
|
||||
const changed = { ...children[42]!, isError: true }
|
||||
const next = { ...block, subCalls: children.map((child, index) => index === 42 ? changed : child) }
|
||||
view.rerender(<ToolCallTree {...initial} node={{ ...initial.node, data: { root: next } }} />)
|
||||
expect(owners.map(owner => owner.callId)).toEqual(['parent', 'child-42'])
|
||||
})
|
||||
|
||||
it('dispatches preparation through the existing keyed toolview with no argument fields', () => {
|
||||
const owners: ToolCallOwnerProps[] = []
|
||||
const view = render(<ToolCallTree {...props({
|
||||
|
||||
Reference in new issue
Block a user