import React from 'react'; import type { Part } from '@opencode-ai/sdk'; import AssistantTextPart from './parts/AssistantTextPart'; import UserTextPart from './parts/UserTextPart'; import ReasoningPart from './parts/ReasoningPart'; import ToolPart from './parts/ToolPart'; import ProgressiveGroup from './parts/ProgressiveGroup'; import MigratingPart from './parts/MigratingPart'; import { MessageFilesDisplay } from '../FileAttachment'; import type { ToolPart as ToolPartType } from '@opencode-ai/sdk'; import type { StreamPhase, ToolPopupContent, AgentMentionInfo } from './types'; import type { TurnGroupingContext } from '../hooks/useTurnGrouping'; import { cn } from '@/lib/utils'; import { isEmptyTextPart, extractTextContent } from './partUtils'; import { FadeInOnReveal } from './FadeInOnReveal'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine } from '@remixicon/react'; import type { ContentChangeReason } from '@/hooks/useChatScrollManager'; import { SimpleMarkdownRenderer } from '../MarkdownRenderer'; import { useMessageStore } from '@/stores/messageStore'; import { useSessionStore } from '@/stores/useSessionStore'; const useMigrationTimer = ( turnGroupingContext: TurnGroupingContext | undefined, previewablePartIds: Set ): { isAnimating: boolean } => { const timerRef = React.useRef(null); const animationTimerRef = React.useRef(null); const [isAnimating, setIsAnimating] = React.useState(false); const contextRef = React.useRef(turnGroupingContext); contextRef.current = turnGroupingContext; const partIdsRef = React.useRef(previewablePartIds); partIdsRef.current = previewablePartIds; const timerStartedRef = React.useRef(false); const hasPreviewableParts = previewablePartIds.size > 0; React.useEffect(() => { if (!turnGroupingContext) return; if (!turnGroupingContext.isWorking) return; if (!hasPreviewableParts) return; if (timerStartedRef.current) return; timerStartedRef.current = true; timerRef.current = window.setTimeout(() => { timerRef.current = null; setIsAnimating(true); animationTimerRef.current = window.setTimeout(() => { animationTimerRef.current = null; setIsAnimating(false); const context = contextRef.current; if (!context) { return; } const idsToPreview = Array.from(partIdsRef.current); if (idsToPreview.length > 0) { context.markPartsPreviewed(idsToPreview); } }, 300); }, 1000); }, [hasPreviewableParts, turnGroupingContext]); React.useEffect(() => { if (!turnGroupingContext) return; if (!turnGroupingContext.isWorking || !hasPreviewableParts) { if (timerRef.current) { window.clearTimeout(timerRef.current); timerRef.current = null; } if (animationTimerRef.current) { window.clearTimeout(animationTimerRef.current); animationTimerRef.current = null; } setIsAnimating(false); timerStartedRef.current = false; } }, [hasPreviewableParts, turnGroupingContext]); React.useEffect(() => { return () => { if (timerRef.current) window.clearTimeout(timerRef.current); if (animationTimerRef.current) window.clearTimeout(animationTimerRef.current); }; }, []); return { isAnimating }; }; const ACTIVITY_STANDALONE_TOOL_NAMES = new Set(['task']); const isActivityStandaloneTool = (toolName: unknown): boolean => { return typeof toolName === 'string' && ACTIVITY_STANDALONE_TOOL_NAMES.has(toolName.toLowerCase()); }; interface MessageBodyProps { messageId: string; parts: Part[]; isUser: boolean; isMessageCompleted: boolean; syntaxTheme: { [key: string]: React.CSSProperties }; isMobile: boolean; hasTouchInput?: boolean; copiedCode: string | null; onCopyCode: (code: string) => void; expandedTools: Set; onToggleTool: (toolId: string) => void; onShowPopup: (content: ToolPopupContent) => void; streamPhase: StreamPhase; allowAnimation: boolean; onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void; shouldShowHeader?: boolean; hasTextContent?: boolean; onCopyMessage?: () => void; copiedMessage?: boolean; onAuxiliaryContentComplete?: () => void; showReasoningTraces?: boolean; agentMention?: AgentMentionInfo; turnGroupingContext?: TurnGroupingContext; onRevert?: () => void; isFirstMessage?: boolean; } const UserMessageBody: React.FC<{ messageId: string; parts: Part[]; isMobile: boolean; hasTouchInput?: boolean; hasTextContent?: boolean; onCopyMessage?: () => void; copiedMessage?: boolean; onShowPopup: (content: ToolPopupContent) => void; agentMention?: AgentMentionInfo; onRevert?: () => void; isFirstMessage?: boolean; }> = ({ messageId, parts, isMobile, hasTouchInput, hasTextContent, onCopyMessage, copiedMessage, onShowPopup, agentMention, onRevert, isFirstMessage }) => { const [copyHintVisible, setCopyHintVisible] = React.useState(false); const copyHintTimeoutRef = React.useRef(null); const textParts = React.useMemo(() => { return parts.filter((part) => { if (part.type !== 'text') return false; return !isEmptyTextPart(part); }); }, [parts]); const mentionToken = agentMention?.token; let mentionInjected = false; const canCopyMessage = Boolean(onCopyMessage); const isMessageCopied = Boolean(copiedMessage); const isTouchContext = Boolean(hasTouchInput ?? isMobile); const hasCopyableText = Boolean(hasTextContent); const clearCopyHintTimeout = React.useCallback(() => { if (copyHintTimeoutRef.current !== null && typeof window !== 'undefined') { window.clearTimeout(copyHintTimeoutRef.current); copyHintTimeoutRef.current = null; } }, []); const revealCopyHint = React.useCallback(() => { if (!isTouchContext || !canCopyMessage || !hasCopyableText || typeof window === 'undefined') { return; } clearCopyHintTimeout(); setCopyHintVisible(true); copyHintTimeoutRef.current = window.setTimeout(() => { setCopyHintVisible(false); copyHintTimeoutRef.current = null; }, 1800); }, [canCopyMessage, clearCopyHintTimeout, hasCopyableText, isTouchContext]); React.useEffect(() => { if (!hasCopyableText) { setCopyHintVisible(false); clearCopyHintTimeout(); } }, [clearCopyHintTimeout, hasCopyableText]); const handleCopyButtonClick = React.useCallback( (event: React.MouseEvent) => { if (!onCopyMessage || !hasCopyableText) { return; } event.stopPropagation(); event.preventDefault(); onCopyMessage(); if (isTouchContext) { revealCopyHint(); } }, [hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint] ); return (
{textParts.map((part, index) => { let mentionForPart: AgentMentionInfo | undefined; if (agentMention && mentionToken && !mentionInjected) { const candidateText = extractTextContent(part); if (candidateText.includes(mentionToken)) { mentionForPart = agentMention; mentionInjected = true; } } return ( ); })}
{(canCopyMessage && hasCopyableText) || (onRevert && !isFirstMessage) ? (
{onRevert && !isFirstMessage && ( Revert from here )} {canCopyMessage && hasCopyableText && ( Copy message )}
) : null}
); }; const AssistantMessageBody: React.FC> = ({ messageId, parts, isMessageCompleted, syntaxTheme, isMobile, hasTouchInput, copiedCode, onCopyCode, expandedTools, onToggleTool, onShowPopup, streamPhase: _streamPhase, allowAnimation: _allowAnimation, onContentChange, shouldShowHeader = true, hasTextContent = false, onCopyMessage, copiedMessage = false, onAuxiliaryContentComplete, showReasoningTraces = false, turnGroupingContext, }) => { void _streamPhase; void _allowAnimation; const [copyHintVisible, setCopyHintVisible] = React.useState(false); const copyHintTimeoutRef = React.useRef(null); const canCopyMessage = Boolean(onCopyMessage); const isMessageCopied = Boolean(copiedMessage); const isTouchContext = Boolean(hasTouchInput ?? isMobile); const awaitingMessageCompletion = !isMessageCompleted; const visibleParts = React.useMemo(() => { return parts .filter((part) => !isEmptyTextPart(part)) .filter((part) => { const rawPart = part as Record; return rawPart.type !== 'compaction'; }); }, [parts]); const toolParts = React.useMemo(() => { return visibleParts.filter((part): part is ToolPartType => part.type === 'tool'); }, [visibleParts]); const assistantTextParts = React.useMemo(() => { return visibleParts.filter((part) => part.type === 'text'); }, [visibleParts]); const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage); const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false; const hasStopFinish = React.useMemo(() => { return parts.some((part) => part.type === 'step-finish' && (part as { reason?: string | null | undefined }).reason === 'stop'); }, [parts]); const hasTools = toolParts.length > 0; const hasPendingTools = React.useMemo(() => { return toolParts.some((toolPart) => { const state = (toolPart as Record).state as Record | undefined ?? {}; const status = state?.status; return status === 'pending' || status === 'running' || status === 'started'; }); }, [toolParts]); const isToolFinalized = React.useCallback((toolPart: ToolPartType) => { const state = (toolPart as Record).state as Record | undefined ?? {}; const status = state?.status; if (status === 'pending' || status === 'running' || status === 'started') { return false; } const time = state?.time as Record | undefined ?? {}; const endTime = typeof time?.end === 'number' ? time.end : undefined; const startTime = typeof time?.start === 'number' ? time.start : undefined; if (typeof endTime !== 'number') { return false; } if (typeof startTime === 'number' && endTime < startTime) { return false; } return true; }, []); const allToolsFinalized = React.useMemo(() => { if (toolParts.length === 0) { return true; } if (hasPendingTools) { return false; } return toolParts.every((toolPart) => isToolFinalized(toolPart)); }, [toolParts, hasPendingTools, isToolFinalized]); const assistantTextReady = React.useMemo(() => { if (assistantTextParts.length === 0) { return true; } return assistantTextParts.every((part) => { const time = (part as Record).time as Record | undefined; return typeof time?.end === 'number'; }); }, [assistantTextParts]); const reasoningParts = React.useMemo(() => { return visibleParts.filter((part) => part.type === 'reasoning'); }, [visibleParts]); const reasoningComplete = React.useMemo(() => { if (reasoningParts.length === 0) { return true; } return reasoningParts.every((part) => { const time = (part as Record).time as { end?: number } | undefined; return typeof time?.end === 'number'; }); }, [reasoningParts]); const stepState = React.useMemo(() => { let stepStarts = 0; let stepFinishes = 0; visibleParts.forEach((part) => { if (part.type === 'step-start') { stepStarts += 1; } else if (part.type === 'step-finish') { stepFinishes += 1; } }); return { stepStarts, stepFinishes, hasOpenStep: stepStarts > stepFinishes, }; }, [visibleParts]); const hasOpenStep = stepState.hasOpenStep; const shouldHoldForReasoning = reasoningParts.length > 0 && hasTools && (hasPendingTools || hasOpenStep || !allToolsFinalized); const shouldCoordinateRendering = React.useMemo(() => { if (!hasTools) { return assistantTextParts.length > 0 ? shouldHoldForReasoning : false; } if (assistantTextParts.length === 0) { return hasOpenStep || hasPendingTools || !allToolsFinalized; } return true; }, [assistantTextParts.length, hasOpenStep, hasPendingTools, hasTools, shouldHoldForReasoning, allToolsFinalized]); const shouldHoldAssistantText = awaitingMessageCompletion || (shouldCoordinateRendering && (!assistantTextReady || !allToolsFinalized || hasPendingTools || hasOpenStep)) || shouldHoldForReasoning; const shouldHoldTools = awaitingMessageCompletion || (hasTools && (hasPendingTools || hasOpenStep || !allToolsFinalized)); const shouldHoldReasoning = awaitingMessageCompletion || shouldHoldForReasoning; const hasAuxiliaryContent = hasTools || reasoningParts.length > 0; const isTextlessAssistantMessage = assistantTextParts.length === 0; const auxiliaryContentComplete = hasAuxiliaryContent && isTextlessAssistantMessage && !shouldHoldTools && !shouldHoldReasoning && allToolsFinalized && reasoningComplete; const auxiliaryCompletionAnnouncedRef = React.useRef(false); const soloReasoningScrollTriggeredRef = React.useRef(false); React.useEffect(() => { soloReasoningScrollTriggeredRef.current = false; }, [messageId]); React.useEffect(() => { if (!auxiliaryContentComplete) { auxiliaryCompletionAnnouncedRef.current = false; return; } if (auxiliaryCompletionAnnouncedRef.current) { return; } auxiliaryCompletionAnnouncedRef.current = true; onAuxiliaryContentComplete?.(); }, [auxiliaryContentComplete, onAuxiliaryContentComplete]); React.useEffect(() => { if (awaitingMessageCompletion) { soloReasoningScrollTriggeredRef.current = false; return; } if (hasTools) { soloReasoningScrollTriggeredRef.current = false; return; } if (reasoningParts.length === 0) { return; } if (shouldHoldReasoning || !reasoningComplete) { return; } if (soloReasoningScrollTriggeredRef.current) { return; } soloReasoningScrollTriggeredRef.current = true; onContentChange?.('structural'); }, [awaitingMessageCompletion, hasTools, onContentChange, reasoningComplete, reasoningParts.length, shouldHoldReasoning]); const hasCopyableText = Boolean(hasTextContent) && !awaitingMessageCompletion; const clearCopyHintTimeout = React.useCallback(() => { if (copyHintTimeoutRef.current !== null && typeof window !== 'undefined') { window.clearTimeout(copyHintTimeoutRef.current); copyHintTimeoutRef.current = null; } }, []); const revealCopyHint = React.useCallback(() => { if (!isTouchContext || !canCopyMessage || !hasCopyableText || typeof window === 'undefined') { return; } clearCopyHintTimeout(); setCopyHintVisible(true); copyHintTimeoutRef.current = window.setTimeout(() => { setCopyHintVisible(false); copyHintTimeoutRef.current = null; }, 1800); }, [canCopyMessage, clearCopyHintTimeout, hasCopyableText, isTouchContext]); React.useEffect(() => { if (!hasCopyableText) { setCopyHintVisible(false); clearCopyHintTimeout(); } }, [clearCopyHintTimeout, hasCopyableText]); const handleCopyButtonClick = React.useCallback( (event: React.MouseEvent) => { if (!onCopyMessage || !hasCopyableText) { return; } event.stopPropagation(); event.preventDefault(); onCopyMessage(); if (isTouchContext) { revealCopyHint(); } }, [hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint] ); const handleForkClick = React.useCallback( (event: React.MouseEvent) => { event.stopPropagation(); event.preventDefault(); if (!createSessionFromAssistantMessage) { return; } void createSessionFromAssistantMessage(messageId); }, [createSessionFromAssistantMessage, messageId] ); React.useEffect(() => { return () => { clearCopyHintTimeout(); }; }, [clearCopyHintTimeout]); const toolConnections = React.useMemo(() => { const connections: Record = {}; const displayableTools = toolParts.filter((toolPart) => { if (isActivityStandaloneTool(toolPart.tool)) { return false; } if (shouldHoldTools) { return false; } return isToolFinalized(toolPart); }); displayableTools.forEach((toolPart, index) => { connections[toolPart.id] = { hasPrev: index > 0, hasNext: index < displayableTools.length - 1, }; }); return connections; }, [toolParts, shouldHoldTools, isToolFinalized]); const activityPartsForTurn = React.useMemo(() => { return turnGroupingContext?.activityParts ?? []; }, [turnGroupingContext]); const activityPartsForMessage = React.useMemo(() => { if (!turnGroupingContext) return []; return activityPartsForTurn.filter((activity) => activity.messageId === messageId); }, [activityPartsForTurn, messageId, turnGroupingContext]); const activityPartsByPart = React.useMemo(() => { const map = new Map(); activityPartsForMessage.forEach((activity) => { map.set(activity.part, activity); }); return map; }, [activityPartsForMessage]); const visibleActivityPartsForTurn = React.useMemo(() => { if (!turnGroupingContext) return []; const base = !showReasoningTraces ? activityPartsForTurn.filter((activity) => activity.kind === 'tool') : activityPartsForTurn; // Tools rendered standalone are excluded from Activity group. return base.filter((activity) => { if (activity.kind !== 'tool') { return true; } const toolName = (activity.part as ToolPartType).tool; return !isActivityStandaloneTool(toolName); }); }, [activityPartsForTurn, showReasoningTraces, turnGroupingContext]); const [hasEverHadMultipleVisibleActivities, setHasEverHadMultipleVisibleActivities] = React.useState(false); React.useEffect(() => { if (!turnGroupingContext) { return; } if (visibleActivityPartsForTurn.length > 1) { setHasEverHadMultipleVisibleActivities(true); } }, [turnGroupingContext, visibleActivityPartsForTurn.length]); const shouldShowActivityGroup = Boolean(turnGroupingContext && hasEverHadMultipleVisibleActivities); const previewableActivityPartsForMessage = React.useMemo(() => { if (!turnGroupingContext) return []; if (!shouldShowActivityGroup) return []; if (!turnGroupingContext.isWorking) { return []; } const previewable: (typeof activityPartsForMessage) = []; activityPartsForMessage.forEach((activity) => { if (turnGroupingContext.previewedPartIds.has(activity.id)) { return; } if (!showReasoningTraces && activity.kind !== 'tool') { return; } const part = activity.part; if (activity.kind === 'tool') { const toolPart = part as ToolPartType; if (isActivityStandaloneTool(toolPart.tool)) { return; } if (shouldHoldTools) return; if (!isToolFinalized(toolPart)) return; } else if (activity.kind === 'reasoning') { if (!showReasoningTraces) return; if (shouldHoldReasoning) return; const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time; if (typeof time?.end !== 'number') return; } else if (activity.kind === 'justification') { if (!showReasoningTraces) return; if (shouldHoldAssistantText) return; const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time; if (typeof time?.end !== 'number') return; } previewable.push(activity); }); return previewable; }, [ activityPartsForMessage, isToolFinalized, shouldHoldAssistantText, shouldHoldReasoning, shouldHoldTools, showReasoningTraces, shouldShowActivityGroup, turnGroupingContext, ]); const previewableActivityPartIds = React.useMemo(() => { const ids = new Set(); previewableActivityPartsForMessage.forEach((activity) => { ids.add(activity.id); }); return ids; }, [previewableActivityPartsForMessage]); const { isAnimating: isMessageAnimating } = useMigrationTimer(turnGroupingContext, previewableActivityPartIds); const shouldRenderActivityGroup = Boolean( turnGroupingContext && turnGroupingContext.activityGroupAnchorMessageId === messageId && shouldShowActivityGroup && visibleActivityPartsForTurn.length > 0 ); const standaloneToolParts = React.useMemo(() => { return toolParts.filter((toolPart) => isActivityStandaloneTool(toolPart.tool)); }, [toolParts]); const isActivityGroupVisibleNow = React.useMemo(() => { if (!turnGroupingContext || !shouldRenderActivityGroup) { return false; } if (!turnGroupingContext.isWorking) { return true; } const previewed = turnGroupingContext.previewedPartIds; return visibleActivityPartsForTurn.some((activity) => previewed.has(activity.id)); }, [shouldRenderActivityGroup, turnGroupingContext, visibleActivityPartsForTurn]); const standaloneToolsFirstVisibleAtRef = React.useRef(null); const activityGroupFirstVisibleAtRef = React.useRef(null); React.useEffect(() => { standaloneToolsFirstVisibleAtRef.current = null; activityGroupFirstVisibleAtRef.current = null; }, [messageId]); const now = Date.now(); if (standaloneToolParts.length > 0 && standaloneToolsFirstVisibleAtRef.current === null) { standaloneToolsFirstVisibleAtRef.current = now; } if (isActivityGroupVisibleNow && activityGroupFirstVisibleAtRef.current === null) { activityGroupFirstVisibleAtRef.current = now; } const shouldPlaceActivityAfterStandaloneTools = Boolean( standaloneToolParts.length > 0 && isActivityGroupVisibleNow && typeof standaloneToolsFirstVisibleAtRef.current === 'number' && typeof activityGroupFirstVisibleAtRef.current === 'number' && activityGroupFirstVisibleAtRef.current > standaloneToolsFirstVisibleAtRef.current ); const renderedParts = React.useMemo(() => { const rendered: React.ReactNode[] = []; const pushActivityGroup = () => { if (!turnGroupingContext || !shouldRenderActivityGroup) { return; } rendered.push( ); }; if (!shouldPlaceActivityAfterStandaloneTools) { pushActivityGroup(); } // Standalone tools: rendered outside Activity group standaloneToolParts.forEach((standaloneToolPart) => { rendered.push( ); }); if (shouldPlaceActivityAfterStandaloneTools) { pushActivityGroup(); } const partsWithTime: Array<{ part: Part; index: number; endTime: number | null; element: React.ReactNode; }> = []; visibleParts.forEach((part, index) => { const activity = activityPartsByPart.get(part); if (!activity) { return; } if (!turnGroupingContext) { return; } let endTime: number | null = null; let element: React.ReactNode | null = null; if (!shouldShowActivityGroup) { if (activity.kind === 'tool') { const toolPart = part as ToolPartType; if (isActivityStandaloneTool(toolPart.tool)) { return; } const toolState = (toolPart as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state; const time = toolState?.time; const isFinalized = isToolFinalized(toolPart); const shouldShowTool = !shouldHoldTools && isFinalized; if (!shouldShowTool) { return; } const connection = toolConnections[toolPart.id]; const toolElement = ( ); element = toolElement; endTime = isFinalized && typeof time?.end === 'number' ? time.end : null; } if (element) { partsWithTime.push({ part, index, endTime, element, }); } return; } if (!turnGroupingContext.isWorking || turnGroupingContext.isGroupExpanded) { return; } if (turnGroupingContext.previewedPartIds.has(activity.id)) { return; } if (!showReasoningTraces && activity.kind !== 'tool') { return; } const wrapForMigration = previewableActivityPartIds.has(activity.id); switch (activity.kind) { case 'tool': { const toolPart = part as ToolPartType; if (isActivityStandaloneTool(toolPart.tool)) { break; } const toolState = (toolPart as { state?: { time?: { end?: number | null | undefined } | null | undefined } | null | undefined }).state; const time = toolState?.time; const isFinalized = isToolFinalized(toolPart); const shouldShowTool = !shouldHoldTools && isFinalized; if (!shouldShowTool) { break; } const connection = toolConnections[toolPart.id]; const toolElement = ( ); element = wrapForMigration ? ( {toolElement} ) : toolElement; endTime = isFinalized && typeof time?.end === 'number' ? time.end : null; break; } case 'reasoning': { if (!showReasoningTraces) { break; } const reasoningTime = (part as { time?: { end?: number | null | undefined } | null | undefined }).time; const hasEndTime = typeof reasoningTime?.end === 'number'; const shouldShowReasoning = hasEndTime && !shouldHoldReasoning; if (!shouldShowReasoning) { break; } const reasoningElement = ( ); element = wrapForMigration ? ( {reasoningElement} ) : reasoningElement; endTime = hasEndTime ? reasoningTime?.end ?? null : null; break; } case 'justification': { if (!showReasoningTraces) { break; } const time = (part as { time?: { end?: number | null | undefined } | null | undefined }).time; const hasEndTime = typeof time?.end === 'number'; const shouldShowJustification = hasEndTime && !shouldHoldAssistantText; if (!shouldShowJustification) { break; } const textElement = ( ); element = wrapForMigration ? ( {textElement} ) : textElement; endTime = hasEndTime ? time?.end ?? null : null; break; } default: break; } if (element) { partsWithTime.push({ part, index, endTime, element, }); } }); partsWithTime.sort((a, b) => { if (a.endTime === null && b.endTime === null) { return a.index - b.index; } if (a.endTime === null) { return 1; } if (b.endTime === null) { return -1; } return a.endTime - b.endTime; }); partsWithTime.forEach(({ element }) => { rendered.push(element); }); return rendered; }, [ activityPartsByPart, copiedCode, copiedMessage, expandedTools, hasTextContent, isMessageAnimating, isMobile, isToolFinalized, messageId, onContentChange, onCopyCode, onCopyMessage, onShowPopup, onToggleTool, previewableActivityPartIds, shouldHoldAssistantText, shouldHoldReasoning, shouldHoldTools, shouldShowActivityGroup, shouldShowHeader, showReasoningTraces, syntaxTheme, toolConnections, turnGroupingContext, visibleActivityPartsForTurn, visibleParts, toolParts, standaloneToolParts, shouldRenderActivityGroup, shouldPlaceActivityAfterStandaloneTools, ]); const userMessageId = turnGroupingContext?.turnId; const currentSessionId = useSessionStore((state) => state.currentSessionId); const rawSummaryBodyFromStore = useMessageStore((state) => { if (!userMessageId || !currentSessionId) return undefined; const sessionMessages = state.messages.get(currentSessionId); if (!sessionMessages) return undefined; const userMsg = sessionMessages.find((m) => m.info?.id === userMessageId); if (!userMsg) return undefined; const summary = (userMsg.info as { summary?: { body?: string | null | undefined } | null | undefined }).summary; const body = summary?.body; return typeof body === 'string' && body.trim().length > 0 ? body : undefined; }); const summaryCandidate = typeof turnGroupingContext?.summaryBody === 'string' && turnGroupingContext.summaryBody.trim().length > 0 ? turnGroupingContext.summaryBody : rawSummaryBodyFromStore; const summaryBodyRef = React.useRef(undefined); if (summaryCandidate && summaryCandidate.trim().length > 0) { summaryBodyRef.current = summaryCandidate; } const prevUserMessageId = React.useRef(userMessageId); if (prevUserMessageId.current !== userMessageId) { prevUserMessageId.current = userMessageId; summaryBodyRef.current = undefined; } const summaryBody = summaryBodyRef.current; const showSummaryBody = turnGroupingContext?.isLastAssistantInTurn && summaryBody && summaryBody.trim().length > 0; const shouldShowFooter = hasTextContent && assistantTextParts.length > 0 && hasStopFinish && isLastAssistantInTurn; const [isSummaryHovered, setIsSummaryHovered] = React.useState(false); const footerButtons = ( <> Start new session from this answer {onCopyMessage && ( Copy answer )} ); return (
{renderedParts} {showSummaryBody && (
setIsSummaryHovered(true)} onMouseLeave={() => setIsSummaryHovered(false)} > {shouldShowFooter && (
{footerButtons}
)}
)}
{!showSummaryBody && shouldShowFooter && (
{footerButtons}
)}
); }; const MessageBody: React.FC = ({ isUser, ...props }) => { if (isUser) { return ( ); } return ; }; export default React.memo(MessageBody);