import { useRef, useEffect, useMemo, memo } from 'react' import { useChatStore } from '../../stores/chatStore' import { useTabStore } from '../../stores/tabStore' import { useTranslation } from '../../i18n' import type { TranslationKey } from '../../i18n/locales/en' 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 { InlineTaskSummary } from './InlineTaskSummary' import type { AgentTaskNotification, UIMessage } from '../../types/chat' type ToolCall = Extract type ToolResult = Extract type RenderItem = | { kind: 'tool_group'; toolCalls: ToolCall[]; id: string } | { kind: 'message'; message: UIMessage } type RenderModel = { renderItems: RenderItem[] toolResultMap: Map childToolCallsByParent: Map } function appendChildToolCall( childToolCallsByParent: Map, parentToolUseId: string, toolCall: ToolCall, ) { const siblings = childToolCallsByParent.get(parentToolUseId) if (siblings) { siblings.push(toolCall) } else { childToolCallsByParent.set(parentToolUseId, [toolCall]) } } export function buildRenderModel(messages: UIMessage[]): RenderModel { const items: RenderItem[] = [] const toolResultMap = new Map() const childToolCallsByParent = new Map() const toolUseIds = new Set() let pendingToolCalls: ToolCall[] = [] const inlineParentToolUseIds = new Set() const flushGroup = (resetInlineParents = false) => { if (pendingToolCalls.length > 0) { items.push({ kind: 'tool_group', toolCalls: [...pendingToolCalls], id: `group-${pendingToolCalls[0]!.id}`, }) for (const toolCall of pendingToolCalls) { inlineParentToolUseIds.add(toolCall.toolUseId) } pendingToolCalls = [] } if (resetInlineParents) { inlineParentToolUseIds.clear() } } for (const msg of messages) { if (msg.type === 'tool_use') { toolUseIds.add(msg.toolUseId) } if (msg.type === 'tool_result') { toolResultMap.set(msg.toolUseId, msg) } } for (const msg of messages) { if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) { continue } if (msg.type === 'tool_use') { const parentIsPending = msg.parentToolUseId ? pendingToolCalls.some((toolCall) => toolCall.toolUseId === msg.parentToolUseId) : false if (msg.parentToolUseId && (inlineParentToolUseIds.has(msg.parentToolUseId) || parentIsPending)) { flushGroup() appendChildToolCall(childToolCallsByParent, msg.parentToolUseId, msg) inlineParentToolUseIds.add(msg.toolUseId) continue } if (msg.toolName === 'AskUserQuestion') { flushGroup(true) items.push({ kind: 'message', message: msg }) } else { pendingToolCalls.push(msg) } } else { flushGroup(true) items.push({ kind: 'message', message: msg }) } } flushGroup() return { renderItems: items, toolResultMap, childToolCallsByParent } } export function MessageList() { const activeTabId = useTabStore((s) => s.activeTabId) const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined) const messages = sessionState?.messages ?? [] const chatState = sessionState?.chatState ?? 'idle' const streamingText = sessionState?.streamingText ?? '' const activeThinkingId = sessionState?.activeThinkingId ?? null const agentTaskNotifications = sessionState?.agentTaskNotifications ?? {} const bottomRef = useRef(null) useEffect(() => { bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' }) }, [messages.length, streamingText]) const { toolResultMap, childToolCallsByParent, renderItems } = useMemo( () => buildRenderModel(messages), [messages], ) return (
{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 } /> ) })} {streamingText && ( )} {/* Show StreamingIndicator when: - tool_executing: tool is running - thinking but no active ThinkingBlock yet: the gap between sending a message and receiving the first thinking delta */} {(chatState === 'tool_executing' || (chatState === 'thinking' && !activeThinkingId)) && ( )}
) } export const MessageBlock = memo(function MessageBlock({ message, activeThinkingId, agentTaskNotifications, toolResult, }: { message: UIMessage activeThinkingId: string | null agentTaskNotifications: Record toolResult?: { content: unknown; isError: boolean } | null }) { const t = useTranslation() switch (message.type) { case 'user_text': return case 'assistant_text': return case 'thinking': return case 'tool_use': if (message.toolName === 'AskUserQuestion') { return ( ) } return ( ) case 'tool_result': return ( ) case 'permission_request': return ( ) case 'error': { const errorKey = message.code ? `error.${message.code}` as TranslationKey : null const errorText = errorKey ? t(errorKey) : null const displayMessage = (errorText && errorText !== errorKey) ? errorText : message.message const showRawDetail = Boolean(message.message) && message.message.trim() !== '' && message.message !== displayMessage return (
Error: {displayMessage} {showRawDetail && (
{message.message}
)}
) } case 'task_summary': return case 'system': return (
{message.content}
) } })