diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 62814bf1..430fb751 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -4,47 +4,98 @@ import { UserMessage } from './UserMessage' import { AssistantMessage } from './AssistantMessage' import { ThinkingBlock } from './ThinkingBlock' import { ToolCallBlock } from './ToolCallBlock' +import { ToolCallGroup } from './ToolCallGroup' import { ToolResultBlock } from './ToolResultBlock' import { PermissionDialog } from './PermissionDialog' import { AskUserQuestion } from './AskUserQuestion' import { StreamingIndicator } from './StreamingIndicator' import type { UIMessage } from '../../types/chat' +type ToolCall = Extract +type ToolResult = Extract + +type RenderItem = + | { kind: 'tool_group'; toolCalls: ToolCall[]; id: string } + | { kind: 'message'; message: UIMessage } + +function buildRenderItems(messages: UIMessage[], toolUseIds: Set): RenderItem[] { + const items: RenderItem[] = [] + let pendingToolCalls: ToolCall[] = [] + + const flushGroup = () => { + if (pendingToolCalls.length > 0) { + items.push({ + kind: 'tool_group', + toolCalls: [...pendingToolCalls], + id: `group-${pendingToolCalls[0]!.id}`, + }) + pendingToolCalls = [] + } + } + + for (const msg of messages) { + if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) { + continue + } + + if (msg.type === 'tool_use') { + if (msg.toolName === 'AskUserQuestion') { + flushGroup() + items.push({ kind: 'message', message: msg }) + } else { + pendingToolCalls.push(msg) + } + } else { + flushGroup() + items.push({ kind: 'message', message: msg }) + } + } + + flushGroup() + return items +} + export function MessageList() { const { messages, chatState, streamingText, activeThinkingId } = useChatStore() const bottomRef = useRef(null) - // Auto-scroll to bottom on new messages useEffect(() => { bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' }) }, [messages.length, streamingText]) - // Build a map of toolUseId → tool_use message for linking results - const toolUseMap = new Map>() + const toolUseIds = new Set() + const toolResultMap = new Map() for (const msg of messages) { if (msg.type === 'tool_use') { - toolUseMap.set(msg.toolUseId, msg) + toolUseIds.add(msg.toolUseId) } - } - - // For each tool_use, find its matching tool_result - const toolResultMap = new Map>() - for (const msg of messages) { if (msg.type === 'tool_result' && msg.toolUseId) { toolResultMap.set(msg.toolUseId, msg) } } + const renderItems = buildRenderItems(messages, toolUseIds) + return (
- {messages.map((msg) => { - // Skip tool_results that are rendered inline within their ToolCallBlock - if (msg.type === 'tool_result' && toolUseMap.has(msg.toolUseId)) { - return null + {renderItems.map((item) => { + if (item.kind === 'tool_group') { + return ( + !toolResultMap.has(tc.toolUseId)) + } + /> + ) } + const msg = item.message return ( { + const r = toolResultMap.get(msg.toolUseId) + return r ? { content: r.content, isError: r.isError } : null + })() : null } /> ) })} - {/* Streaming text (not yet committed as a message) */} {streamingText && chatState === 'streaming' && ( )} - {/* Loading indicator */} {chatState !== 'idle' && chatState !== 'streaming' && chatState !== 'permission_pending' && ( )} @@ -92,7 +144,6 @@ function MessageBlock({ case 'thinking': return case 'tool_use': - // Special handling for AskUserQuestion tool calls if (message.toolName === 'AskUserQuestion') { return ( ) case 'tool_result': - // Standalone tool_result (no matching tool_use found) return ( +
Error: {message.message}
) case 'system': return ( -
+
{message.content}
)