import { useRef, useEffect } from 'react' import { useChatStore } from '../../stores/chatStore' 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 { 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) useEffect(() => { bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' }) }, [messages.length, streamingText]) const toolUseIds = new Set() const toolResultMap = new Map() for (const msg of messages) { if (msg.type === 'tool_use') { toolUseIds.add(msg.toolUseId) } if (msg.type === 'tool_result' && msg.toolUseId) { toolResultMap.set(msg.toolUseId, msg) } } const renderItems = buildRenderItems(messages, toolUseIds) 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 && chatState === 'streaming' && ( )} {/* 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)) && ( )}
) } function MessageBlock({ message, activeThinkingId, toolResult, }: { message: UIMessage activeThinkingId: string | null 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 return (
Error: {displayMessage}
) } case 'task_summary': return case 'system': return (
{message.content}
) } }