import React, { useState, useEffect } from "react"; import { Terminal, User, Bot, AlertCircle, CheckCircle2 } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { TodoWidget, LSWidget, ReadWidget, ReadResultWidget, GlobWidget, BashWidget, WriteWidget, GrepWidget, EditWidget, EditResultWidget, MCPWidget, CommandWidget, CommandOutputWidget, SummaryWidget, MultiEditWidget, MultiEditResultWidget, SystemReminderWidget, SystemInitializedWidget, TaskWidget, LSResultWidget, ThinkingWidget } from "./ToolWidgets"; interface StreamMessageProps { message: ClaudeStreamMessage; className?: string; streamMessages: ClaudeStreamMessage[]; onLinkDetected?: (url: string) => void; } /** * Component to render a single Claude Code stream message */ const StreamMessageComponent: React.FC = ({ message, className, streamMessages, onLinkDetected }) => { // State to track tool results mapped by tool call ID const [toolResults, setToolResults] = useState>(new Map()); // Extract all tool results from stream messages useEffect(() => { const results = new Map(); // Iterate through all messages to find tool results streamMessages.forEach(msg => { if (msg.type === "user" && msg.message?.content && Array.isArray(msg.message.content)) { msg.message.content.forEach((content: any) => { if (content.type === "tool_result" && content.tool_use_id) { results.set(content.tool_use_id, content); } }); } }); setToolResults(results); }, [streamMessages]); // Helper to get tool result for a specific tool call ID const getToolResult = (toolId: string | undefined): any => { if (!toolId) return null; return toolResults.get(toolId) || null; }; // Debug logging to understand message structure console.log('[StreamMessage] Rendering message:', { type: message.type, hasMessage: !!message.message, messageStructure: message.message ? Object.keys(message.message) : 'no message field', fullMessage: message }); try { // Skip rendering for meta messages that don't have meaningful content if (message.isMeta && !message.leafUuid && !message.summary) { return null; } // Handle summary messages if (message.leafUuid && message.summary && (message as any).type === "summary") { return ; } // System initialization message if (message.type === "system" && message.subtype === "init") { return ( ); } // Assistant message if (message.type === "assistant" && message.message) { const msg = message.message; let renderedSomething = false; const renderedCard = (
{msg.content && Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => { // Text content - render as markdown if (content.type === "text") { // Ensure we have a string to render const textContent = typeof content.text === 'string' ? content.text : (content.text?.text || JSON.stringify(content.text || content)); renderedSomething = true; return (
{String(children).replace(/\n$/, '')} ) : ( {children} ); } }} > {textContent}
); } // Thinking content - render with ThinkingWidget if (content.type === "thinking") { renderedSomething = true; return (
); } // Tool use - render custom widgets based on tool name if (content.type === "tool_use") { const toolName = content.name?.toLowerCase(); const input = content.input; const toolId = content.id; // Get the tool result if available const toolResult = getToolResult(toolId); // Function to render the appropriate tool widget const renderToolWidget = () => { // Task tool - for sub-agent tasks if (toolName === "task" && input) { renderedSomething = true; return ; } // Edit tool if (toolName === "edit" && input?.file_path) { renderedSomething = true; return ; } // MultiEdit tool if (toolName === "multiedit" && input?.file_path && input?.edits) { renderedSomething = true; return ; } // MCP tools (starting with mcp__) if (content.name?.startsWith("mcp__")) { renderedSomething = true; return ; } // TodoWrite tool if (toolName === "todowrite" && input?.todos) { renderedSomething = true; return ; } // LS tool if (toolName === "ls" && input?.path) { renderedSomething = true; return ; } // Read tool if (toolName === "read" && input?.file_path) { renderedSomething = true; return ; } // Glob tool if (toolName === "glob" && input?.pattern) { renderedSomething = true; return ; } // Bash tool if (toolName === "bash" && input?.command) { renderedSomething = true; return ; } // Write tool if (toolName === "write" && input?.file_path && input?.content) { renderedSomething = true; return ; } // Grep tool if (toolName === "grep" && input?.pattern) { renderedSomething = true; return ; } // Default - return null return null; }; // Render the tool widget const widget = renderToolWidget(); if (widget) { renderedSomething = true; return
{widget}
; } // Fallback to basic tool display renderedSomething = true; return (
Using tool: {content.name}
{content.input && (
                              {JSON.stringify(content.input, null, 2)}
                            
)}
); } return null; })} {msg.usage && (
Tokens: {msg.usage.input_tokens} in, {msg.usage.output_tokens} out
)}
); if (!renderedSomething) return null; return renderedCard; } // User message - handle both nested and direct content structures if (message.type === "user") { // Don't render meta messages, which are for system use if (message.isMeta) return null; // Handle different message structures const msg = message.message || message; let renderedSomething = false; const renderedCard = (
{/* Handle content that is a simple string (e.g. from user commands) */} {(typeof msg.content === 'string' || (msg.content && !Array.isArray(msg.content))) && ( (() => { const contentStr = typeof msg.content === 'string' ? msg.content : String(msg.content); if (contentStr.trim() === '') return null; renderedSomething = true; // Check if it's a command message const commandMatch = contentStr.match(/(.+?)<\/command-name>[\s\S]*?(.+?)<\/command-message>[\s\S]*?(.*?)<\/command-args>/); if (commandMatch) { const [, commandName, commandMessage, commandArgs] = commandMatch; return ( ); } // Check if it's command output const stdoutMatch = contentStr.match(/([\s\S]*?)<\/local-command-stdout>/); if (stdoutMatch) { const [, output] = stdoutMatch; return ; } // Otherwise render as plain text return (
{contentStr}
); })() )} {/* Handle content that is an array of parts */} {Array.isArray(msg.content) && msg.content.map((content: any, idx: number) => { // Tool result if (content.type === "tool_result") { // Skip duplicate tool_result if a dedicated widget is present let hasCorrespondingWidget = false; if (content.tool_use_id && streamMessages) { for (let i = streamMessages.length - 1; i >= 0; i--) { const prevMsg = streamMessages[i]; if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id); if (toolUse) { const toolName = toolUse.name?.toLowerCase(); const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep']; if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { hasCorrespondingWidget = true; } break; } } } } if (hasCorrespondingWidget) { return null; } // Extract the actual content string let contentText = ''; if (typeof content.content === 'string') { contentText = content.content; } else if (content.content && typeof content.content === 'object') { // Handle object with text property if (content.content.text) { contentText = content.content.text; } else if (Array.isArray(content.content)) { // Handle array of content blocks contentText = content.content .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) .join('\n'); } else { // Fallback to JSON stringify contentText = JSON.stringify(content.content, null, 2); } } // Always show system reminders regardless of widget status const reminderMatch = contentText.match(/(.*?)<\/system-reminder>/s); if (reminderMatch) { const reminderMessage = reminderMatch[1].trim(); const beforeReminder = contentText.substring(0, reminderMatch.index || 0).trim(); const afterReminder = contentText.substring((reminderMatch.index || 0) + reminderMatch[0].length).trim(); renderedSomething = true; return (
Tool Result
{beforeReminder && (
                                {beforeReminder}
                              
)}
{afterReminder && (
                                {afterReminder}
                              
)}
); } // Check if this is an Edit tool result const isEditResult = contentText.includes("has been updated. Here's the result of running `cat -n`"); if (isEditResult) { renderedSomething = true; return (
Edit Result
); } // Check if this is a MultiEdit tool result const isMultiEditResult = contentText.includes("has been updated with multiple edits") || contentText.includes("MultiEdit completed successfully") || contentText.includes("Applied multiple edits to"); if (isMultiEditResult) { renderedSomething = true; return (
MultiEdit Result
); } // Check if this is an LS tool result (directory tree structure) const isLSResult = (() => { if (!content.tool_use_id || typeof contentText !== 'string') return false; // Check if this result came from an LS tool by looking for the tool call let isFromLSTool = false; // Search in previous assistant messages for the matching tool_use if (streamMessages) { for (let i = streamMessages.length - 1; i >= 0; i--) { const prevMsg = streamMessages[i]; // Only check assistant messages if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id && c.name?.toLowerCase() === 'ls' ); if (toolUse) { isFromLSTool = true; break; } } } } // Only proceed if this is from an LS tool if (!isFromLSTool) return false; // Additional validation: check for tree structure pattern const lines = contentText.split('\n'); const hasTreeStructure = lines.some(line => /^\s*-\s+/.test(line)); const hasNoteAtEnd = lines.some(line => line.trim().startsWith('NOTE: do any of the files')); return hasTreeStructure || hasNoteAtEnd; })(); if (isLSResult) { renderedSomething = true; return (
Directory Contents
); } // Check if this is a Read tool result (contains line numbers with arrow separator) const isReadResult = content.tool_use_id && typeof contentText === 'string' && /^\s*\d+→/.test(contentText); if (isReadResult) { // Try to find the corresponding Read tool call to get the file path let filePath: string | undefined; // Search in previous assistant messages for the matching tool_use if (streamMessages) { for (let i = streamMessages.length - 1; i >= 0; i--) { const prevMsg = streamMessages[i]; // Only check assistant messages if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id && c.name?.toLowerCase() === 'read' ); if (toolUse?.input?.file_path) { filePath = toolUse.input.file_path; break; } } } } renderedSomething = true; return (
Read Result
); } // Handle empty tool results if (!contentText || contentText.trim() === '') { renderedSomething = true; return (
Tool Result
Tool did not return any output
); } renderedSomething = true; return (
{content.is_error ? ( ) : ( )} Tool Result
                            {contentText}
                          
); } // Text content if (content.type === "text") { // Handle both string and object formats const textContent = typeof content.text === 'string' ? content.text : (content.text?.text || JSON.stringify(content.text)); renderedSomething = true; return (
{textContent}
); } return null; })}
); if (!renderedSomething) return null; return renderedCard; } // Result message - render with markdown if (message.type === "result") { const isError = message.is_error || message.subtype?.includes("error"); return (
{isError ? ( ) : ( )}

{isError ? "Execution Failed" : "Execution Complete"}

{message.result && (
{String(children).replace(/\n$/, '')} ) : ( {children} ); } }} > {message.result}
)} {message.error && (
{message.error}
)}
{(message.cost_usd !== undefined || message.total_cost_usd !== undefined) && (
Cost: ${((message.cost_usd || message.total_cost_usd)!).toFixed(4)} USD
)} {message.duration_ms !== undefined && (
Duration: {(message.duration_ms / 1000).toFixed(2)}s
)} {message.num_turns !== undefined && (
Turns: {message.num_turns}
)} {message.usage && (
Total tokens: {message.usage.input_tokens + message.usage.output_tokens} ({message.usage.input_tokens} in, {message.usage.output_tokens} out)
)}
); } // Skip rendering if no meaningful content return null; } catch (error) { // If any error occurs during rendering, show a safe error message console.error("Error rendering stream message:", error, message); return (

Error rendering message

{error instanceof Error ? error.message : 'Unknown error'}

); } }; export const StreamMessage = React.memo(StreamMessageComponent);