diff --git a/shared/i18n/locales/en/chat.json b/shared/i18n/locales/en/chat.json index 33f50c43..6b923e74 100644 --- a/shared/i18n/locales/en/chat.json +++ b/shared/i18n/locales/en/chat.json @@ -54,6 +54,9 @@ "tool": "Tool", "expandTool": "Expand tool result", "collapseTool": "Collapse tool result", + "expandToolGroup": "Expand tool calls", + "collapseToolGroup": "Collapse tool calls", + "toolGroupSummary": "{{count}} tool calls", "permission": "Permission", "plan": "Plan", "running": "Running", diff --git a/shared/i18n/locales/ja/chat.json b/shared/i18n/locales/ja/chat.json index 6ebade9c..b926200f 100644 --- a/shared/i18n/locales/ja/chat.json +++ b/shared/i18n/locales/ja/chat.json @@ -54,6 +54,9 @@ "tool": "ツール", "expandTool": "ツール結果を展開", "collapseTool": "ツール結果を折りたたむ", + "expandToolGroup": "ツール呼び出しを展開", + "collapseToolGroup": "ツール呼び出しを折りたたむ", + "toolGroupSummary": "ツール呼び出し {{count}} 件", "permission": "権限", "plan": "計画", "running": "実行中", diff --git a/shared/i18n/locales/ru/chat.json b/shared/i18n/locales/ru/chat.json index c879cee7..7df3ca69 100644 --- a/shared/i18n/locales/ru/chat.json +++ b/shared/i18n/locales/ru/chat.json @@ -54,6 +54,9 @@ "tool": "Инструмент", "expandTool": "Развернуть результат инструмента", "collapseTool": "Свернуть результат инструмента", + "expandToolGroup": "Развернуть вызовы инструментов", + "collapseToolGroup": "Свернуть вызовы инструментов", + "toolGroupSummary": "Вызовов инструментов: {{count}}", "permission": "Разрешение", "plan": "План", "running": "Выполняется", diff --git a/shared/i18n/locales/zh/chat.json b/shared/i18n/locales/zh/chat.json index b22db1c3..cfd1d134 100644 --- a/shared/i18n/locales/zh/chat.json +++ b/shared/i18n/locales/zh/chat.json @@ -54,6 +54,9 @@ "tool": "工具", "expandTool": "展开工具结果", "collapseTool": "折叠工具结果", + "expandToolGroup": "展开工具调用", + "collapseToolGroup": "折叠工具调用", + "toolGroupSummary": "{{count}} 个工具调用", "permission": "权限", "plan": "计划", "running": "进行中", diff --git a/src/pages/Chat/AcpAssistantTurn.tsx b/src/pages/Chat/AcpAssistantTurn.tsx index efbaee3e..cf60db36 100644 --- a/src/pages/Chat/AcpAssistantTurn.tsx +++ b/src/pages/Chat/AcpAssistantTurn.tsx @@ -7,10 +7,64 @@ import { AcpPermissionCard } from './AcpPermissionCard'; import { AcpPlanItem } from './AcpPlanItem'; import { AcpThoughtBlock } from './AcpThoughtBlock'; import { AcpToolCallCard } from './AcpToolCallCard'; +import { AcpToolCallsGroup } from './AcpToolCallsGroup'; import type { AcpTurnFileSummary } from '@/lib/acp/openclaw-file-activities'; import { AcpTurnFileActivity } from './AcpTurnFileActivity'; import { AcpAttachmentPart } from './AcpAttachmentPart'; import type { AcpTurnTiming } from '@/lib/acp/turn-timings'; +import type { TimelineItem, ToolCallItem } from '@/lib/acp/timeline-types'; + +type TurnRenderItem = + | TimelineItem + | { kind: 'tool-call-group'; id: string; items: ToolCallItem[] }; + +function partitionTurnItems(items: TimelineItem[]): TurnRenderItem[] { + const result: TurnRenderItem[] = []; + let toolRun: ToolCallItem[] = []; + + const flushTools = () => { + if (toolRun.length === 0) return; + if (toolRun.length === 1) { + result.push(toolRun[0]); + } else { + result.push({ kind: 'tool-call-group', id: `tool-group:${toolRun[0].id}`, items: [...toolRun] }); + } + toolRun = []; + }; + + for (const item of items) { + if (item.kind === 'tool-call') { + toolRun.push(item); + continue; + } + flushTools(); + result.push(item); + } + flushTools(); + return result; +} + +function isToolGroupSettled( + toolItems: ToolCallItem[], + timing: AcpTurnTiming | undefined, + turnItems: TimelineItem[], +): boolean { + if (timing?.status === 'complete') return true; + if (timing?.status === 'running') return false; + if (toolItems.length > 0 && toolItems.every((item) => item.historical)) return true; + + const allFinished = toolItems.every((item) => item.status === 'completed' || item.status === 'failed'); + if (!allFinished) return false; + + const lastToolId = toolItems[toolItems.length - 1]?.id; + const lastToolIndex = turnItems.findIndex((item) => item.id === lastToolId); + const hasAssistantReplyAfter = turnItems.slice(lastToolIndex + 1).some( + (item) => item.kind === 'message-segment' && item.role === 'assistant', + ); + if (hasAssistantReplyAfter) return true; + + return turnItems.every((item) => item.kind === 'tool-call'); +} function assistantTurnClipboardText(group: AcpAssistantTurnDisplayGroup): string { const textSegments: string[] = []; @@ -76,6 +130,7 @@ export function AcpAssistantTurn({ onPermissionSelect?: (requestId: string, optionId: string) => void; }) { const clipboardText = useMemo(() => assistantTurnClipboardText(group), [group]); + const renderItems = useMemo(() => partitionTurnItems(group.items), [group.items]); return (