import { useRef, useEffect, useMemo, memo, useState, useCallback, useLayoutEffect, type ReactNode } from 'react' import { ArrowDown, BookMarked, Settings } from 'lucide-react' import { ApiError } from '../../api/client' import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions' import { useChatStore } from '../../stores/chatStore' import { useWorkspaceChatContextStore } from '../../stores/workspaceChatContextStore' import { SETTINGS_TAB_ID, useTabStore } from '../../stores/tabStore' import { useTeamStore } from '../../stores/teamStore' import { useUIStore } from '../../stores/uiStore' 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 { CurrentTurnChangeCard } from './CurrentTurnChangeCard' import type { AgentTaskNotification, UIMessage } from '../../types/chat' import { ConfirmDialog } from '../shared/ConfirmDialog' type ToolCall = Extract type ToolResult = Extract type MemoryEvent = Extract type RenderItem = | { kind: 'tool_group'; toolCalls: ToolCall[]; id: string } | { kind: 'message'; message: UIMessage } type RenderModel = { renderItems: RenderItem[] toolResultMap: Map childToolCallsByParent: Map } type RewindTurnTarget = { messageId: string userMessageIndex: number content: string expectedContent: string attachments?: Extract['attachments'] } type TurnChangeCardModel = { target: RewindTurnTarget checkpoint: SessionTurnCheckpoint workDir: string | null isLatest: boolean } type ChatMessageRole = 'user' | 'assistant' type ChatSelectionState = { text: string x: number y: number } const CHAT_SELECTION_MENU_OFFSET = 8 function clampValue(value: number, min: number, max: number) { return Math.max(min, Math.min(value, max)) } function getElementForNode(node: Node | null): Element | null { if (!node) return null return node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement } function getChatSelectionPosition(range: Range, root: HTMLElement, pointer: { clientX: number; clientY: number }) { const rect = typeof range.getBoundingClientRect === 'function' ? range.getBoundingClientRect() : null const rootRect = root.getBoundingClientRect() const pointerInsideRoot = pointer.clientX >= rootRect.left && pointer.clientX <= rootRect.right && pointer.clientY >= rootRect.top && pointer.clientY <= rootRect.bottom const fallbackX = rect && rect.width > 0 ? rect.left + rect.width / 2 : rect?.left ?? rootRect.left + 24 const fallbackY = rect ? rect.bottom + CHAT_SELECTION_MENU_OFFSET : rootRect.top + 24 const unclampedX = pointerInsideRoot ? pointer.clientX : fallbackX const unclampedY = pointerInsideRoot ? pointer.clientY + CHAT_SELECTION_MENU_OFFSET : fallbackY const minX = Math.max(12, rootRect.left + 8) const maxX = Math.max(minX, Math.min(window.innerWidth - 160, rootRect.right - 136)) const minY = Math.max(12, rootRect.top + 8) const maxY = Math.max(minY, Math.min(window.innerHeight - 48, rootRect.bottom - 40)) return { x: clampValue(unclampedX, minX, maxX), y: clampValue(unclampedY, minY, maxY), } } function getChatSelectionFromContainer( root: HTMLElement | null, pointer: { clientX: number; clientY: number }, ): ChatSelectionState | null { if (!root) return null const selection = window.getSelection() if (!selection || selection.isCollapsed || selection.rangeCount === 0) return null const range = selection.getRangeAt(0) const startElement = getElementForNode(range.startContainer) const endElement = getElementForNode(range.endContainer) if (!startElement || !endElement || !root.contains(startElement) || !root.contains(endElement)) { return null } const text = selection.toString().trim() if (!text) return null return { ...getChatSelectionPosition(range, root, pointer), text, } } function ChatSelectionMenu({ selection, onAdd, }: { selection: ChatSelectionState | null onAdd: () => void }) { const t = useTranslation() if (!selection) return null return ( ) } function SelectableChatMessage({ sessionId, messageId, role, content, children, }: { sessionId?: string | null messageId: string role: ChatMessageRole content: string children: ReactNode }) { const rootRef = useRef(null) const addReference = useWorkspaceChatContextStore((state) => state.addReference) const [selectionMenu, setSelectionMenu] = useState(null) const t = useTranslation() const sourceName = role === 'assistant' ? t('chat.assistantMessageReference') : t('chat.userMessageReference') useEffect(() => { setSelectionMenu(null) }, [content, messageId]) const addCurrentSelectionToChat = useCallback(() => { if (!sessionId || !selectionMenu) return addReference(sessionId, { kind: 'chat-selection', path: `chat://${role}/${messageId}`, name: sourceName, quote: selectionMenu.text, sourceRole: role, messageId, }) setSelectionMenu(null) window.getSelection()?.removeAllRanges() }, [addReference, messageId, role, selectionMenu, sessionId, sourceName]) return (
{ setSelectionMenu(getChatSelectionFromContainer(rootRef.current, event)) }} onKeyDown={(event) => { if (event.key === 'Escape') setSelectionMenu(null) }} > {children}
) } 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 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_use') { toolUseIds.add(msg.toolUseId) } if (msg.type === 'tool_result') { toolResultMap.set(msg.toolUseId, msg) } } for (const msg of messages) { if (msg.type === 'assistant_text' && !msg.content.trim()) { continue } if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) { continue } if (msg.type === 'tool_result' && msg.parentToolUseId && toolUseIds.has(msg.parentToolUseId)) { continue } if (msg.type === 'tool_use') { if (msg.parentToolUseId && toolUseIds.has(msg.parentToolUseId)) { flushGroup() appendChildToolCall(childToolCallsByParent, msg.parentToolUseId, msg) continue } 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 { renderItems: items, toolResultMap, childToolCallsByParent } } function isTurnResponseMessage(message: UIMessage) { return ( message.type === 'assistant_text' || message.type === 'tool_use' || message.type === 'tool_result' || message.type === 'error' || message.type === 'task_summary' ) } export function getCompletedTurnTargets(messages: UIMessage[]): RewindTurnTarget[] { let userMessageIndex = -1 const completedTurns: RewindTurnTarget[] = [] let currentTarget: RewindTurnTarget | null = null let hasResponseForCurrentTarget = false for (const message of messages) { if (message.type === 'user_text' && !message.pending) { if (currentTarget && hasResponseForCurrentTarget) { completedTurns.push(currentTarget) } userMessageIndex += 1 currentTarget = { messageId: message.id, userMessageIndex, content: message.content, expectedContent: message.modelContent ?? message.content, attachments: message.attachments, } hasResponseForCurrentTarget = false continue } if (currentTarget && isTurnResponseMessage(message)) { hasResponseForCurrentTarget = true } } if (currentTarget && hasResponseForCurrentTarget) { completedTurns.push(currentTarget) } return completedTurns } export function getLatestCompletedTurnTarget(messages: UIMessage[]): RewindTurnTarget | null { const completedTurns = getCompletedTurnTargets(messages) return completedTurns.length > 0 ? completedTurns[completedTurns.length - 1] ?? null : null } function buildTurnCardInsertionMap( renderItems: RenderItem[], turnChangeCards: TurnChangeCardModel[], ) { const lastResponseIndexByTurnId = new Map() const userIndexByTurnId = new Map() let activeTurnId: string | null = null renderItems.forEach((item, index) => { if (item.kind === 'message' && item.message.type === 'user_text' && !item.message.pending) { activeTurnId = item.message.id userIndexByTurnId.set(activeTurnId, index) return } if (activeTurnId) { lastResponseIndexByTurnId.set(activeTurnId, index) } }) const cardsByRenderIndex = new Map() turnChangeCards.forEach((card) => { const renderIndex = lastResponseIndexByTurnId.get(card.target.messageId) ?? userIndexByTurnId.get(card.target.messageId) if (renderIndex === undefined) return const existing = cardsByRenderIndex.get(renderIndex) if (existing) { existing.push(card) } else { cardsByRenderIndex.set(renderIndex, [card]) } }) return cardsByRenderIndex } function getApiErrorMessage(error: unknown) { return error instanceof ApiError ? typeof error.body === 'object' && error.body && 'message' in error.body ? String((error.body as { message: unknown }).message) : error.message : error instanceof Error ? error.message : String(error) } function isSessionTurnCheckpoint(value: unknown): value is SessionTurnCheckpoint { if (!value || typeof value !== 'object') return false const checkpoint = value as Partial return ( Boolean(checkpoint.target) && typeof checkpoint.target?.targetUserMessageId === 'string' && typeof checkpoint.target?.userMessageIndex === 'number' && Boolean(checkpoint.code) && typeof checkpoint.code?.available === 'boolean' && Array.isArray(checkpoint.code?.filesChanged) ) } function normalizeTurnCheckpoints(response: unknown): SessionTurnCheckpoint[] { if (!response || typeof response !== 'object') return [] const checkpoints = (response as { checkpoints?: unknown }).checkpoints if (!Array.isArray(checkpoints)) return [] return checkpoints.filter(isSessionTurnCheckpoint) } function memoryFileLabel(path: string) { const normalized = path.replace(/\\/g, '/') return normalized.split('/').pop() || normalized } function openMemorySettings(path?: string) { const ui = useUIStore.getState() if (path) ui.setPendingMemoryPath(path) ui.setPendingSettingsTab('memory') useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings') } function MemoryEventCard({ message }: { message: MemoryEvent }) { const t = useTranslation() const visibleFiles = message.files.slice(0, 3) const hiddenCount = Math.max(0, message.files.length - visibleFiles.length) return (
{t('chat.memorySavedTitle', { count: message.files.length })}
{message.message ? (
{message.message}
) : null}
{visibleFiles.map((file) => ( {memoryFileLabel(file.path)} ))} {hiddenCount > 0 ? ( {t('chat.memoryMoreFiles', { count: hiddenCount })} ) : null}
) } type MessageListProps = { sessionId?: string | null compact?: boolean } const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 48 const MAX_SCROLL_SNAPSHOTS = 100 const VIRTUALIZE_MIN_ITEMS = 160 const VIRTUAL_INITIAL_ITEMS = 96 const CHAT_SCROLL_AREA_CLASS = [ 'chat-scroll-area', '[scrollbar-width:auto]', '[scrollbar-color:color-mix(in_srgb,var(--color-outline)_72%,transparent)_transparent]', '[&::-webkit-scrollbar]:w-2.5', '[&::-webkit-scrollbar-track]:bg-transparent', '[&::-webkit-scrollbar-thumb]:rounded-full', '[&::-webkit-scrollbar-thumb]:border-[3px]', '[&::-webkit-scrollbar-thumb]:border-transparent', '[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--color-outline)_74%,transparent)]', '[&::-webkit-scrollbar-thumb]:bg-clip-content', '[&::-webkit-scrollbar-thumb:hover]:border-2', '[&::-webkit-scrollbar-thumb:hover]:bg-[color-mix(in_srgb,var(--color-outline)_90%,transparent)]', ].join(' ') type SessionScrollSnapshot = { scrollTop: number wasAtBottom: boolean } const sessionScrollSnapshots = new Map() function isNearScrollBottom(element: HTMLElement) { return ( element.scrollHeight - element.scrollTop - element.clientHeight <= AUTO_SCROLL_BOTTOM_THRESHOLD_PX ) } function rememberSessionScroll(sessionId: string, element: HTMLElement) { if (sessionScrollSnapshots.size >= MAX_SCROLL_SNAPSHOTS && !sessionScrollSnapshots.has(sessionId)) { const oldestSessionId = sessionScrollSnapshots.keys().next().value if (oldestSessionId) { sessionScrollSnapshots.delete(oldestSessionId) } } sessionScrollSnapshots.set(sessionId, { scrollTop: element.scrollTop, wasAtBottom: isNearScrollBottom(element), }) } function clampScrollTop(element: HTMLElement, scrollTop: number) { return Math.max(0, Math.min(scrollTop, getBottomScrollTop(element))) } function getBottomScrollTop(element: HTMLElement) { return Math.max(0, element.scrollHeight - element.clientHeight) } function getRenderItemKey(item: RenderItem) { return item.kind === 'tool_group' ? item.id : item.message.id } export function MessageList({ sessionId, compact = false }: MessageListProps = {}) { const activeTabId = useTabStore((s) => s.activeTabId) const resolvedSessionId = sessionId ?? activeTabId const sessionState = useChatStore((s) => resolvedSessionId ? s.sessions[resolvedSessionId] : undefined, ) const stopGeneration = useChatStore((s) => s.stopGeneration) const reloadHistory = useChatStore((s) => s.reloadHistory) const queueComposerPrefill = useChatStore((s) => s.queueComposerPrefill) const isMemberSession = useTeamStore((s) => resolvedSessionId ? Boolean(s.getMemberBySessionId(resolvedSessionId)) : false, ) const addToast = useUIStore((s) => s.addToast) const messages = sessionState?.messages ?? [] const chatState = sessionState?.chatState ?? 'idle' const streamingText = sessionState?.streamingText ?? '' const activeThinkingId = sessionState?.activeThinkingId ?? null const agentTaskNotifications = sessionState?.agentTaskNotifications ?? {} const scrollContainerRef = useRef(null) const scrollContentRef = useRef(null) const shouldAutoScrollRef = useRef(true) const isProgrammaticScrollingRef = useRef(false) const lastSessionIdRef = useRef(resolvedSessionId) const lastTailMessageIdBySessionRef = useRef(new Map()) const updateVirtualWindowRef = useRef<((element: HTMLElement) => void) | null>(null) const t = useTranslation() const [turnChangeCards, setTurnChangeCards] = useState([]) const [turnChangeLoadError, setTurnChangeLoadError] = useState(null) const [turnActionErrors, setTurnActionErrors] = useState>({}) const [isLoadingTurnChangeCards, setIsLoadingTurnChangeCards] = useState(false) const [rewindingTurnId, setRewindingTurnId] = useState(null) const [turnUndoConfirmTargetId, setTurnUndoConfirmTargetId] = useState(null) const [showJumpToLatest, setShowJumpToLatest] = useState(false) const scrollToBottom = useCallback((behavior: ScrollBehavior) => { shouldAutoScrollRef.current = true isProgrammaticScrollingRef.current = true const container = scrollContainerRef.current const targetScrollTop = container ? getBottomScrollTop(container) : null if (container) { const nextScrollTop = targetScrollTop ?? 0 if (typeof container.scrollTo === 'function') { try { container.scrollTo({ top: nextScrollTop, behavior }) } catch { container.scrollTo(0, nextScrollTop) } } container.scrollTop = nextScrollTop } if (container && resolvedSessionId) { sessionScrollSnapshots.set(resolvedSessionId, { scrollTop: getBottomScrollTop(container), wasAtBottom: true, }) } setShowJumpToLatest(false) // Reset flag after the scroll event(s) from scrollIntoView have fired requestAnimationFrame(() => { const latestContainer = scrollContainerRef.current if ( shouldAutoScrollRef.current && latestContainer && ( targetScrollTop === null || latestContainer.scrollTop === targetScrollTop || isNearScrollBottom(latestContainer) ) ) { latestContainer.scrollTop = getBottomScrollTop(latestContainer) if (resolvedSessionId) { sessionScrollSnapshots.set(resolvedSessionId, { scrollTop: getBottomScrollTop(latestContainer), wasAtBottom: true, }) } } isProgrammaticScrollingRef.current = false }) }, [resolvedSessionId]) const updateAutoScrollState = useCallback(() => { // Ignore scroll events triggered by our own programmatic scrolling to // prevent the jump-to-latest button from flickering during auto-scroll. if (isProgrammaticScrollingRef.current) return const container = scrollContainerRef.current if (!container) return const isAtBottom = isNearScrollBottom(container) shouldAutoScrollRef.current = isAtBottom setShowJumpToLatest(!isAtBottom) if (resolvedSessionId) { rememberSessionScroll(resolvedSessionId, container) } updateVirtualWindowRef.current?.(container) }, [resolvedSessionId]) useLayoutEffect(() => { if (lastSessionIdRef.current !== resolvedSessionId) { const snapshot = resolvedSessionId ? sessionScrollSnapshots.get(resolvedSessionId) : undefined shouldAutoScrollRef.current = snapshot?.wasAtBottom ?? true lastSessionIdRef.current = resolvedSessionId const container = scrollContainerRef.current if (container && snapshot && !snapshot.wasAtBottom) { container.scrollTop = clampScrollTop(container, snapshot.scrollTop) setShowJumpToLatest(true) } else { scrollToBottom('auto') } } }, [resolvedSessionId, scrollToBottom]) const tailMessage = messages[messages.length - 1] ?? null const tailMessageId = tailMessage?.id ?? null const tailMessageType = tailMessage?.type ?? null useEffect(() => { if (!resolvedSessionId) return const previousTailMessageId = lastTailMessageIdBySessionRef.current.get(resolvedSessionId) lastTailMessageIdBySessionRef.current.set(resolvedSessionId, tailMessageId) if (previousTailMessageId === undefined || previousTailMessageId === tailMessageId) return if (tailMessageType === 'user_text' && chatState !== 'idle') { scrollToBottom('auto') } }, [chatState, resolvedSessionId, scrollToBottom, tailMessageId, tailMessageType]) useEffect(() => { if (!shouldAutoScrollRef.current) { setShowJumpToLatest(true) return } scrollToBottom('auto') }, [messages.length, resolvedSessionId, scrollToBottom, streamingText]) const handleJumpToLatest = useCallback(() => { scrollToBottom('auto') }, [scrollToBottom]) useEffect(() => { const content = scrollContentRef.current if (!content || typeof ResizeObserver === 'undefined') return const observer = new ResizeObserver(() => { if (!shouldAutoScrollRef.current) return scrollToBottom('auto') }) observer.observe(content) return () => observer.disconnect() }, [scrollToBottom]) const { toolResultMap, childToolCallsByParent, renderItems } = useMemo( () => buildRenderModel(messages), [messages], ) const shouldVirtualize = renderItems.length > VIRTUALIZE_MIN_ITEMS const [virtualRange, setVirtualRange] = useState(() => ({ start: 0, end: 0 })) const updateVirtualWindow = useCallback((container: HTMLElement | null) => { if (!shouldVirtualize) { setVirtualRange((current) => current.start === 0 && current.end === renderItems.length ? current : { start: 0, end: renderItems.length }, ) return } if (!container || shouldAutoScrollRef.current) { const start = Math.max(0, renderItems.length - VIRTUAL_INITIAL_ITEMS) setVirtualRange((current) => current.start === start && current.end === renderItems.length ? current : { start, end: renderItems.length }, ) return } setVirtualRange((current) => current.start === 0 && current.end === renderItems.length ? current : { start: 0, end: renderItems.length }, ) }, [renderItems.length, shouldVirtualize]) useEffect(() => { updateVirtualWindowRef.current = updateVirtualWindow return () => { if (updateVirtualWindowRef.current === updateVirtualWindow) { updateVirtualWindowRef.current = null } } }, [updateVirtualWindow]) useLayoutEffect(() => { updateVirtualWindow(scrollContainerRef.current) }, [renderItems.length, resolvedSessionId, updateVirtualWindow]) const visibleRenderItems = shouldVirtualize ? renderItems.slice(virtualRange.start, virtualRange.end) : renderItems const topVirtualSpacerHeight = shouldVirtualize ? Math.max(0, virtualRange.start * 96) : 0 const bottomVirtualSpacerHeight = shouldVirtualize ? Math.max(0, renderItems.length - virtualRange.end) * 96 : 0 const completedTurnTargets = useMemo(() => getCompletedTurnTargets(messages), [messages]) const latestCompletedTurnId = completedTurnTargets.length > 0 ? completedTurnTargets[completedTurnTargets.length - 1]?.messageId ?? null : null const turnCardsByRenderIndex = useMemo( () => buildTurnCardInsertionMap(renderItems, turnChangeCards), [renderItems, turnChangeCards], ) const confirmTurnCard = useMemo( () => turnChangeCards.find((card) => card.target.messageId === turnUndoConfirmTargetId) ?? null, [turnChangeCards, turnUndoConfirmTargetId], ) useEffect(() => { if (!resolvedSessionId || completedTurnTargets.length === 0 || isMemberSession) { setTurnChangeCards([]) setTurnChangeLoadError(null) setIsLoadingTurnChangeCards(false) return } if (chatState !== 'idle') { setTurnChangeLoadError(null) setIsLoadingTurnChangeCards(false) return } let cancelled = false setIsLoadingTurnChangeCards(true) setTurnChangeLoadError(null) Promise.all([ sessionsApi.getTurnCheckpoints(resolvedSessionId), sessionsApi.getWorkspaceStatus(resolvedSessionId).catch(() => null), ]) .then(([checkpointResponse, workspaceStatus]) => { if (cancelled) return const targetByMessageId = new Map( completedTurnTargets.map((target) => [target.messageId, target] as const), ) const targetByUserMessageIndex = new Map( completedTurnTargets.map((target) => [target.userMessageIndex, target] as const), ) setTurnChangeCards( normalizeTurnCheckpoints(checkpointResponse).flatMap((checkpoint) => { const target = targetByMessageId.get(checkpoint.target.targetUserMessageId) ?? targetByUserMessageIndex.get(checkpoint.target.userMessageIndex) if (!target || !checkpoint.code.available || checkpoint.code.filesChanged.length === 0) { return [] } return [{ target, checkpoint, workDir: checkpoint.workDir ?? workspaceStatus?.workDir ?? null, isLatest: target.messageId === latestCompletedTurnId, }] }), ) }) .catch((error) => { if (cancelled) return setTurnChangeCards([]) setTurnChangeLoadError(getApiErrorMessage(error)) }) .finally(() => { if (!cancelled) { setIsLoadingTurnChangeCards(false) } }) return () => { cancelled = true } }, [chatState, completedTurnTargets, isMemberSession, latestCompletedTurnId, resolvedSessionId]) const handleUndoCurrentTurn = useCallback(async () => { if (!resolvedSessionId || !confirmTurnCard || rewindingTurnId) return const target = confirmTurnCard.target setRewindingTurnId(target.messageId) setTurnActionErrors((current) => { if (!(target.messageId in current)) return current const next = { ...current } delete next[target.messageId] return next }) try { if (chatState !== 'idle') { stopGeneration(resolvedSessionId) } const result = await sessionsApi.rewind(resolvedSessionId, { targetUserMessageId: target.messageId, userMessageIndex: target.userMessageIndex, expectedContent: target.expectedContent, }) await reloadHistory(resolvedSessionId) queueComposerPrefill(resolvedSessionId, { text: target.content, attachments: target.attachments, }) addToast({ type: 'success', message: result.code.available ? t('chat.rewindSuccessWithCode', { count: result.conversation.messagesRemoved, }) : t('chat.rewindSuccessConversationOnly', { count: result.conversation.messagesRemoved, }), }) setTurnUndoConfirmTargetId(null) } catch (error) { setTurnActionErrors((current) => ({ ...current, [target.messageId]: getApiErrorMessage(error), })) setTurnUndoConfirmTargetId(null) } finally { setRewindingTurnId(null) } }, [ addToast, chatState, confirmTurnCard, queueComposerPrefill, reloadHistory, resolvedSessionId, rewindingTurnId, stopGeneration, t, ]) return (
{topVirtualSpacerHeight > 0 && ( ) } export const MessageBlock = memo(function MessageBlock({ sessionId, message, activeThinkingId, agentTaskNotifications, toolResult, }: { sessionId?: string | null 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 'memory_event': return case 'system': return (
{message.content}
) } })