import React, { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import SessionProviderLogo from '../../../SessionProviderLogo'; import type { ChatMessage, ClaudePermissionSuggestion, PermissionGrantResult, Provider, } from '../../types/types'; import { Markdown } from './Markdown'; import { formatUsageLimitText } from '../../utils/chatFormatting'; import { getClaudePermissionSuggestion } from '../../utils/chatPermissions'; import type { Project } from '../../../../types/app'; import { ToolRenderer, shouldHideToolResult } from '../../tools'; type DiffLine = { type: string; content: string; lineNum: number; }; interface MessageComponentProps { message: ChatMessage; index: number; prevMessage: ChatMessage | null; createDiff: (oldStr: string, newStr: string) => DiffLine[]; onFileOpen?: (filePath: string, diffInfo?: unknown) => void; onShowSettings?: () => void; onGrantToolPermission?: (suggestion: ClaudePermissionSuggestion) => PermissionGrantResult | null | undefined; autoExpandTools?: boolean; showRawParameters?: boolean; showThinking?: boolean; selectedProject?: Project | null; provider: Provider | string; } type InteractiveOption = { number: string; text: string; isSelected: boolean; }; type PermissionGrantState = 'idle' | 'granted' | 'error'; const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => { const { t } = useTranslation('chat'); const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || (prevMessage.type === 'user') || (prevMessage.type === 'tool') || (prevMessage.type === 'error')); const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); const permissionSuggestion = getClaudePermissionSuggestion(message, provider); const [permissionGrantState, setPermissionGrantState] = React.useState('idle'); React.useEffect(() => { setPermissionGrantState('idle'); }, [permissionSuggestion?.entry, message.toolId]); React.useEffect(() => { const node = messageRef.current; if (!autoExpandTools || !node || !message.isToolUse) return; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && !isExpanded) { setIsExpanded(true); const details = node.querySelectorAll('details'); details.forEach((detail) => { detail.open = true; }); } }); }, { threshold: 0.1 } ); observer.observe(node); return () => { observer.unobserve(node); }; }, [autoExpandTools, isExpanded, message.isToolUse]); const formattedTime = useMemo(() => new Date(message.timestamp).toLocaleTimeString(), [message.timestamp]); return (
{message.type === 'user' ? ( /* User message bubble on the right */
{message.content}
{message.images && message.images.length > 0 && (
{message.images.map((img, idx) => ( {img.name} window.open(img.data, '_blank')} /> ))}
)}
{formattedTime}
{!isGrouped && (
U
)}
) : ( /* Claude/Error/Tool messages on the left */
{!isGrouped && (
{message.type === 'error' ? (
!
) : message.type === 'tool' ? (
🔧
) : (
)}
{message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : (provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude'))}
)}
{message.isToolUse ? ( <>
{String(message.displayText || '')}
{message.toolInput && ( )} {/* Tool Result Section */} {message.toolResult && !shouldHideToolResult(message.toolName || 'UnknownTool', message.toolResult) && ( message.toolResult.isError ? ( // Error results - red error box with content
Error
{String(message.toolResult.content || '')} {permissionSuggestion && (
{onShowSettings && ( )}
Adds {permissionSuggestion.entry} to Allowed Tools.
{permissionGrantState === 'error' && (
Unable to update permissions. Please try again.
)} {(permissionSuggestion.isAllowed || permissionGrantState === 'granted') && (
Permission saved. Retry the request to use the tool.
)}
)}
) : ( // Non-error results - route through ToolRenderer (single source of truth)
) )} ) : message.isInteractivePrompt ? ( // Special handling for interactive prompts

Interactive Prompt

{(() => { const lines = (message.content || '').split('\n').filter((line) => line.trim()); const questionLine = lines.find((line) => line.includes('?')) || lines[0] || ''; const options: InteractiveOption[] = []; // Parse the menu options lines.forEach((line) => { // Match lines like "❯ 1. Yes" or " 2. No" const optionMatch = line.match(/[❯\s]*(\d+)\.\s+(.+)/); if (optionMatch) { const isSelected = line.includes('❯'); options.push({ number: optionMatch[1], text: optionMatch[2].trim(), isSelected }); } }); return ( <>

{questionLine}

{/* Option buttons */}
{options.map((option) => ( ))}

⏳ Waiting for your response in the CLI

Please select an option in your terminal where Claude is running.

); })()}
) : message.isThinking ? ( /* Thinking messages - collapsible by default */
💭 Thinking...
{message.content}
) : (
{/* Thinking accordion for reasoning */} {showThinking && message.reasoning && (
💭 Thinking...
{message.reasoning}
)} {(() => { const content = formatUsageLimitText(String(message.content || '')); // Detect if content is pure JSON (starts with { or [) const trimmedContent = content.trim(); if ((trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) && (trimmedContent.endsWith('}') || trimmedContent.endsWith(']'))) { try { const parsed = JSON.parse(trimmedContent); const formatted = JSON.stringify(parsed, null, 2); return (
JSON Response
                              
                                {formatted}
                              
                            
); } catch { // Not valid JSON, fall through to normal rendering } } // Normal rendering for non-JSON content return message.type === 'assistant' ? ( {content} ) : (
{content}
); })()}
)} {!isGrouped && (
{formattedTime}
)}
)}
); }); export default MessageComponent;