import React from 'react'; import type { Part } from '@opencode-ai/sdk/v2'; import { MarkdownRenderer } from '../../MarkdownRenderer'; import type { StreamPhase, ToolPopupContent } from '../types'; import type { ContentChangeReason } from '@/hooks/useChatAutoFollow'; import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle'; import { resolveAssistantDisplayText, shouldRenderAssistantText } from './assistantTextVisibility'; import { streamPerfCount, streamPerfObserve } from '@/stores/utils/streamDebug'; import { GeneratedJsonResultCard } from './GeneratedJsonResultCard'; import { parseGeneratedJsonResult } from './generatedJsonResult'; type PartWithText = Part & { text?: string; content?: string; value?: string; time?: { start?: number; end?: number } }; interface AssistantTextPartProps { part: Part; sessionId?: string; messageId: string; streamPhase: StreamPhase; chatRenderMode?: 'sorted' | 'live'; onContentChange?: (reason?: ContentChangeReason, messageId?: string) => void; onShowPopup?: (content: ToolPopupContent) => void; } const AssistantTextPart: React.FC = ({ part, messageId, streamPhase, chatRenderMode = 'live', onShowPopup, }) => { // Use part directly from props — parent provides the latest version from the store. // No store subscription here to avoid re-render cascade from unrelated delta events. const partWithText = part as PartWithText; const rawText = typeof partWithText.text === 'string' ? partWithText.text : ''; const contentText = typeof partWithText.content === 'string' ? partWithText.content : ''; const valueText = typeof partWithText.value === 'string' ? partWithText.value : ''; const textContent = [rawText, contentText, valueText].reduce((best, candidate) => { return candidate.length > best.length ? candidate : best; }, ''); const isStreamingPhase = streamPhase === 'streaming'; const isCooldownPhase = streamPhase === 'cooldown'; const isStreaming = chatRenderMode === 'live' && (isStreamingPhase || isCooldownPhase); streamPerfCount('ui.assistant_text_part.render'); if (isStreaming) { streamPerfCount('ui.assistant_text_part.render.streaming'); } const throttledTextContent = useStreamingTextThrottle({ text: textContent, isStreaming, identityKey: `${messageId}:${part.id ?? 'text'}`, }); const displayTextContent = resolveAssistantDisplayText({ textContent, throttledTextContent, isStreaming, }); streamPerfObserve('ui.assistant_text_part.display_len', displayTextContent.length); const time = partWithText.time; const isFinalized = Boolean(time && typeof time.end !== 'undefined'); const isRenderableTextPart = part.type === 'text' || part.type === 'reasoning'; if (!isRenderableTextPart) { return null; } if (!shouldRenderAssistantText({ displayTextContent, isFinalized, })) { return null; } const generatedResult = !isStreaming && isFinalized ? parseGeneratedJsonResult(displayTextContent) : null; if (generatedResult) { return (
); } return (
); }; export default React.memo(AssistantTextPart);