import React, { useState } from "react"; import { CheckCircle2, Circle, Clock, FolderOpen, FileText, Search, Terminal, FileEdit, Code, ChevronRight, Maximize2, GitBranch, X, Info, AlertCircle, Settings, Fingerprint, Cpu, FolderSearch, List, LogOut, Edit3, FilePlus, Book, BookOpen, Globe, ListChecks, ListPlus, Globe2, Package, ChevronDown, Package2, Wrench, CheckSquare, type LucideIcon, Sparkles, Bot, Zap, FileCode, Folder, ChevronUp, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { claudeSyntaxTheme } from "@/lib/claudeSyntaxTheme"; import { Button } from "@/components/ui/button"; import { createPortal } from "react-dom"; import * as Diff from 'diff'; import { Card, CardContent } from "@/components/ui/card"; import { detectLinks, makeLinksClickable } from "@/lib/linkDetector"; /** * Widget for TodoWrite tool - displays a beautiful TODO list */ export const TodoWidget: React.FC<{ todos: any[]; result?: any }> = ({ todos, result: _result }) => { const statusIcons = { completed: , in_progress: , pending: }; const priorityColors = { high: "bg-red-500/10 text-red-500 border-red-500/20", medium: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", low: "bg-green-500/10 text-green-500 border-green-500/20" }; return (
Todo List
{todos.map((todo, idx) => (
{statusIcons[todo.status as keyof typeof statusIcons] || statusIcons.pending}

{todo.content}

{todo.priority && ( {todo.priority} )}
))}
); }; /** * Widget for LS (List Directory) tool */ export const LSWidget: React.FC<{ path: string; result?: any }> = ({ path, result }) => { // If we have a result, show it using the LSResultWidget if (result) { let resultContent = ''; if (typeof result.content === 'string') { resultContent = result.content; } else if (result.content && typeof result.content === 'object') { if (result.content.text) { resultContent = result.content.text; } else if (Array.isArray(result.content)) { resultContent = result.content .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) .join('\n'); } else { resultContent = JSON.stringify(result.content, null, 2); } } return (
Directory contents for: {path}
{resultContent && }
); } return (
Listing directory: {path} {!result && (
Loading...
)}
); }; /** * Widget for LS tool result - displays directory tree structure */ export const LSResultWidget: React.FC<{ content: string }> = ({ content }) => { const [expandedDirs, setExpandedDirs] = useState>(new Set()); // Parse the directory tree structure const parseDirectoryTree = (rawContent: string) => { const lines = rawContent.split('\n'); const entries: Array<{ path: string; name: string; type: 'file' | 'directory'; level: number; }> = []; let currentPath: string[] = []; for (const line of lines) { // Skip NOTE section and everything after it if (line.startsWith('NOTE:')) { break; } // Skip empty lines if (!line.trim()) continue; // Calculate indentation level const indent = line.match(/^(\s*)/)?.[1] || ''; const level = Math.floor(indent.length / 2); // Extract the entry name const entryMatch = line.match(/^\s*-\s+(.+?)(\/$)?$/); if (!entryMatch) continue; const fullName = entryMatch[1]; const isDirectory = line.trim().endsWith('/'); const name = isDirectory ? fullName : fullName; // Update current path based on level currentPath = currentPath.slice(0, level); currentPath.push(name); entries.push({ path: currentPath.join('/'), name, type: isDirectory ? 'directory' : 'file', level, }); } return entries; }; const entries = parseDirectoryTree(content); const toggleDirectory = (path: string) => { setExpandedDirs(prev => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }; // Group entries by parent for collapsible display const getChildren = (parentPath: string, parentLevel: number) => { return entries.filter(e => { if (e.level !== parentLevel + 1) return false; const parentParts = parentPath.split('/').filter(Boolean); const entryParts = e.path.split('/').filter(Boolean); // Check if this entry is a direct child of the parent if (entryParts.length !== parentParts.length + 1) return false; // Check if all parent parts match for (let i = 0; i < parentParts.length; i++) { if (parentParts[i] !== entryParts[i]) return false; } return true; }); }; const renderEntry = (entry: typeof entries[0], isRoot = false) => { const hasChildren = entry.type === 'directory' && entries.some(e => e.path.startsWith(entry.path + '/') && e.level === entry.level + 1); const isExpanded = expandedDirs.has(entry.path) || isRoot; const getIcon = () => { if (entry.type === 'directory') { return isExpanded ? : ; } // File type icons based on extension const ext = entry.name.split('.').pop()?.toLowerCase(); switch (ext) { case 'rs': return ; case 'toml': case 'yaml': case 'yml': case 'json': return ; case 'md': return ; case 'js': case 'jsx': case 'ts': case 'tsx': return ; case 'py': return ; case 'go': return ; case 'sh': case 'bash': return ; default: return ; } }; return (
entry.type === 'directory' && hasChildren && toggleDirectory(entry.path)} > {entry.type === 'directory' && hasChildren && ( )} {(!hasChildren || entry.type !== 'directory') && (
)} {getIcon()} {entry.name}
{entry.type === 'directory' && hasChildren && isExpanded && (
{getChildren(entry.path, entry.level).map(child => renderEntry(child))}
)}
); }; // Get root entries const rootEntries = entries.filter(e => e.level === 0); return (
{rootEntries.map(entry => renderEntry(entry, true))}
); }; /** * Widget for Read tool */ export const ReadWidget: React.FC<{ filePath: string; result?: any }> = ({ filePath, result }) => { // If we have a result, show it using the ReadResultWidget if (result) { let resultContent = ''; if (typeof result.content === 'string') { resultContent = result.content; } else if (result.content && typeof result.content === 'object') { if (result.content.text) { resultContent = result.content.text; } else if (Array.isArray(result.content)) { resultContent = result.content .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) .join('\n'); } else { resultContent = JSON.stringify(result.content, null, 2); } } return (
File content: {filePath}
{resultContent && }
); } return (
Reading file: {filePath} {!result && (
Loading...
)}
); }; /** * Widget for Read tool result - shows file content with line numbers */ export const ReadResultWidget: React.FC<{ content: string; filePath?: string }> = ({ content, filePath }) => { const [isExpanded, setIsExpanded] = useState(false); // Extract file extension for syntax highlighting const getLanguage = (path?: string) => { if (!path) return "text"; const ext = path.split('.').pop()?.toLowerCase(); const languageMap: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", py: "python", rs: "rust", go: "go", java: "java", cpp: "cpp", c: "c", cs: "csharp", php: "php", rb: "ruby", swift: "swift", kt: "kotlin", scala: "scala", sh: "bash", bash: "bash", zsh: "bash", yaml: "yaml", yml: "yaml", json: "json", xml: "xml", html: "html", css: "css", scss: "scss", sass: "sass", less: "less", sql: "sql", md: "markdown", toml: "ini", ini: "ini", dockerfile: "dockerfile", makefile: "makefile" }; return languageMap[ext || ""] || "text"; }; // Parse content to separate line numbers from code const parseContent = (rawContent: string) => { const lines = rawContent.split('\n'); const codeLines: string[] = []; let minLineNumber = Infinity; // First, determine if the content is likely a numbered list from the 'read' tool. // It is if more than half the non-empty lines match the expected format. const nonEmptyLines = lines.filter(line => line.trim() !== ''); if (nonEmptyLines.length === 0) { return { codeContent: rawContent, startLineNumber: 1 }; } const parsableLines = nonEmptyLines.filter(line => /^\s*\d+→/.test(line)).length; const isLikelyNumbered = (parsableLines / nonEmptyLines.length) > 0.5; if (!isLikelyNumbered) { return { codeContent: rawContent, startLineNumber: 1 }; } // If it's a numbered list, parse it strictly. for (const line of lines) { // Remove leading whitespace before parsing const trimmedLine = line.trimStart(); const match = trimmedLine.match(/^(\d+)→(.*)$/); if (match) { const lineNum = parseInt(match[1], 10); if (minLineNumber === Infinity) { minLineNumber = lineNum; } // Preserve the code content exactly as it appears after the arrow codeLines.push(match[2]); } else if (line.trim() === '') { // Preserve empty lines codeLines.push(''); } else { // If a line in a numbered block does not match, it's a formatting anomaly. // Render it as a blank line to avoid showing the raw, un-parsed string. codeLines.push(''); } } // Remove trailing empty lines while (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') { codeLines.pop(); } return { codeContent: codeLines.join('\n'), startLineNumber: minLineNumber === Infinity ? 1 : minLineNumber }; }; const language = getLanguage(filePath); const { codeContent, startLineNumber } = parseContent(content); const lineCount = content.split('\n').filter(line => line.trim()).length; const isLargeFile = lineCount > 20; return (
{filePath || "File content"} {isLargeFile && ( ({lineCount} lines) )}
{isLargeFile && ( )}
{(!isLargeFile || isExpanded) && (
{codeContent}
)} {isLargeFile && !isExpanded && (
Click "Expand" to view the full file
)}
); }; /** * Widget for Glob tool */ export const GlobWidget: React.FC<{ pattern: string; result?: any }> = ({ pattern, result }) => { // Extract result content if available let resultContent = ''; let isError = false; if (result) { isError = result.is_error || false; if (typeof result.content === 'string') { resultContent = result.content; } else if (result.content && typeof result.content === 'object') { if (result.content.text) { resultContent = result.content.text; } else if (Array.isArray(result.content)) { resultContent = result.content .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) .join('\n'); } else { resultContent = JSON.stringify(result.content, null, 2); } } } return (
Searching for pattern: {pattern} {!result && (
Searching...
)}
{/* Show result if available */} {result && (
{resultContent || (isError ? "Search failed" : "No matches found")}
)}
); }; /** * Widget for Bash tool */ export const BashWidget: React.FC<{ command: string; description?: string; result?: any; }> = ({ command, description, result }) => { // Extract result content if available let resultContent = ''; let isError = false; if (result) { isError = result.is_error || false; if (typeof result.content === 'string') { resultContent = result.content; } else if (result.content && typeof result.content === 'object') { if (result.content.text) { resultContent = result.content.text; } else if (Array.isArray(result.content)) { resultContent = result.content .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) .join('\n'); } else { resultContent = JSON.stringify(result.content, null, 2); } } } return (
Terminal {description && ( <> {description} )} {/* Show loading indicator when no result yet */} {!result && (
Running...
)}
$ {command} {/* Show result if available */} {result && (
{resultContent || (isError ? "Command failed" : "Command completed")}
)}
); }; /** * Widget for Write tool */ export const WriteWidget: React.FC<{ filePath: string; content: string; result?: any }> = ({ filePath, content, result: _result }) => { const [isMaximized, setIsMaximized] = useState(false); // Extract file extension for syntax highlighting const getLanguage = (path: string) => { const ext = path.split('.').pop()?.toLowerCase(); const languageMap: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", py: "python", rs: "rust", go: "go", java: "java", cpp: "cpp", c: "c", cs: "csharp", php: "php", rb: "ruby", swift: "swift", kt: "kotlin", scala: "scala", sh: "bash", bash: "bash", zsh: "bash", yaml: "yaml", yml: "yaml", json: "json", xml: "xml", html: "html", css: "css", scss: "scss", sass: "sass", less: "less", sql: "sql", md: "markdown", toml: "ini", ini: "ini", dockerfile: "dockerfile", makefile: "makefile" }; return languageMap[ext || ""] || "text"; }; const language = getLanguage(filePath); const isLargeContent = content.length > 1000; const displayContent = isLargeContent ? content.substring(0, 1000) + "\n..." : content; // Maximized view as a modal const MaximizedView = () => { if (!isMaximized) return null; return createPortal(
{/* Backdrop with blur */}
setIsMaximized(false)} /> {/* Modal content */}
{/* Header */}
{filePath}
{/* Code content */}
{content}
, document.body ); }; const CodePreview = ({ codeContent, truncated }: { codeContent: string; truncated: boolean }) => (
Preview {isLargeContent && truncated && (
Truncated to 1000 chars
)}
{codeContent}
); return (
Writing to file: {filePath}
); }; /** * Widget for Grep tool */ export const GrepWidget: React.FC<{ pattern: string; include?: string; path?: string; exclude?: string; result?: any; }> = ({ pattern, include, path, exclude, result: _result }) => { return (
Searching with grep
Pattern: {pattern}
{path && (
Path: {path}
)} {include && (
Include: {include}
)} {exclude && (
Exclude: {exclude}
)}
); }; const getLanguage = (path: string) => { const ext = path.split('.').pop()?.toLowerCase(); const languageMap: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", py: "python", rs: "rust", go: "go", java: "java", cpp: "cpp", c: "c", cs: "csharp", php: "php", rb: "ruby", swift: "swift", kt: "kotlin", scala: "scala", sh: "bash", bash: "bash", zsh: "bash", yaml: "yaml", yml: "yaml", json: "json", xml: "xml", html: "html", css: "css", scss: "scss", sass: "sass", less: "less", sql: "sql", md: "markdown", toml: "ini", ini: "ini", dockerfile: "dockerfile", makefile: "makefile" }; return languageMap[ext || ""] || "text"; }; /** * Widget for Edit tool - shows the edit operation */ export const EditWidget: React.FC<{ file_path: string; old_string: string; new_string: string; result?: any; }> = ({ file_path, old_string, new_string, result: _result }) => { const diffResult = Diff.diffLines(old_string || '', new_string || '', { newlineIsToken: true, ignoreWhitespace: false }); const language = getLanguage(file_path); return (
Applying Edit to: {file_path}
{diffResult.map((part, index) => { const partClass = part.added ? 'bg-green-950/20' : part.removed ? 'bg-red-950/20' : ''; if (!part.added && !part.removed && part.count && part.count > 8) { return (
... {part.count} unchanged lines ...
); } const value = part.value.endsWith('\n') ? part.value.slice(0, -1) : part.value; return (
{part.added ? + : part.removed ? - : null}
{value}
); })}
); }; /** * Widget for Edit tool result - shows a diff view */ export const EditResultWidget: React.FC<{ content: string }> = ({ content }) => { // Parse the content to extract file path and code snippet const lines = content.split('\n'); let filePath = ''; const codeLines: { lineNumber: string; code: string }[] = []; let inCodeBlock = false; for (const rawLine of lines) { const line = rawLine.replace(/\r$/, ''); if (line.includes('The file') && line.includes('has been updated')) { const match = line.match(/The file (.+) has been updated/); if (match) { filePath = match[1]; } } else if (/^\s*\d+/.test(line)) { inCodeBlock = true; const lineMatch = line.match(/^\s*(\d+)\t?(.*)$/); if (lineMatch) { const [, lineNum, codePart] = lineMatch; codeLines.push({ lineNumber: lineNum, code: codePart, }); } } else if (inCodeBlock) { // Allow non-numbered lines inside a code block (for empty lines) codeLines.push({ lineNumber: '', code: line }); } } const codeContent = codeLines.map(l => l.code).join('\n'); const firstNumberedLine = codeLines.find(l => l.lineNumber !== ''); const startLineNumber = firstNumberedLine ? parseInt(firstNumberedLine.lineNumber) : 1; const language = getLanguage(filePath); return (
Edit Result {filePath && ( <> {filePath} )}
{codeContent}
); }; /** * Widget for MCP (Model Context Protocol) tools */ export const MCPWidget: React.FC<{ toolName: string; input?: any; result?: any; }> = ({ toolName, input, result: _result }) => { const [isExpanded, setIsExpanded] = useState(false); // Parse the tool name to extract components // Format: mcp__namespace__method const parts = toolName.split('__'); const namespace = parts[1] || ''; const method = parts[2] || ''; // Format namespace for display (handle kebab-case and snake_case) const formatNamespace = (ns: string) => { return ns .replace(/-/g, ' ') .replace(/_/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; // Format method name const formatMethod = (m: string) => { return m .replace(/_/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; const hasInput = input && Object.keys(input).length > 0; const inputString = hasInput ? JSON.stringify(input, null, 2) : ''; const isLargeInput = inputString.length > 200; // Count tokens approximation (very rough estimate) const estimateTokens = (str: string) => { // Rough approximation: ~4 characters per token return Math.ceil(str.length / 4); }; const inputTokens = hasInput ? estimateTokens(inputString) : 0; return (
{/* Header */}
MCP Tool
{hasInput && (
~{inputTokens} tokens {isLargeInput && ( )}
)}
{/* Tool Path */}
MCP {formatNamespace(namespace)}
{formatMethod(method)} ()
{/* Input Parameters */} {hasInput && (
Parameters
{inputString}
{/* Gradient fade for collapsed view */} {!isExpanded && isLargeInput && (
)}
{/* Expand hint */} {!isExpanded && isLargeInput && (
)}
)} {/* No input message */} {!hasInput && (
No parameters required
)}
); }; /** * Widget for user commands (e.g., model, clear) */ export const CommandWidget: React.FC<{ commandName: string; commandMessage: string; commandArgs?: string; }> = ({ commandName, commandMessage, commandArgs }) => { return (
Command
$ {commandName} {commandArgs && ( {commandArgs} )}
{commandMessage && commandMessage !== commandName && (
{commandMessage}
)}
); }; /** * Widget for command output/stdout */ export const CommandOutputWidget: React.FC<{ output: string; onLinkDetected?: (url: string) => void; }> = ({ output, onLinkDetected }) => { // Check for links on mount and when output changes React.useEffect(() => { if (output && onLinkDetected) { const links = detectLinks(output); if (links.length > 0) { // Notify about the first detected link onLinkDetected(links[0].fullUrl); } } }, [output, onLinkDetected]); // Parse ANSI codes for basic styling const parseAnsiToReact = (text: string) => { // Simple ANSI parsing - handles bold (\u001b[1m) and reset (\u001b[22m) const parts = text.split(/(\u001b\[\d+m)/); let isBold = false; const elements: React.ReactNode[] = []; parts.forEach((part, idx) => { if (part === '\u001b[1m') { isBold = true; return; } else if (part === '\u001b[22m') { isBold = false; return; } else if (part.match(/\u001b\[\d+m/)) { // Ignore other ANSI codes for now return; } if (!part) return; // Make links clickable within this part const linkElements = makeLinksClickable(part, (url) => { onLinkDetected?.(url); }); if (isBold) { elements.push( {linkElements} ); } else { elements.push(...linkElements); } }); return elements; }; return (
Output
          {output ? parseAnsiToReact(output) : No output}
        
); }; /** * Widget for AI-generated summaries */ export const SummaryWidget: React.FC<{ summary: string; leafUuid?: string; }> = ({ summary, leafUuid }) => { return (
AI Summary

{summary}

{leafUuid && (
ID: {leafUuid.slice(0, 8)}...
)}
); }; /** * Widget for displaying MultiEdit tool usage */ export const MultiEditWidget: React.FC<{ file_path: string; edits: Array<{ old_string: string; new_string: string }>; result?: any; }> = ({ file_path, edits, result: _result }) => { const [isExpanded, setIsExpanded] = useState(false); const language = getLanguage(file_path); return (
Using tool: MultiEdit
{file_path}
{isExpanded && (
{edits.map((edit, index) => { const diffResult = Diff.diffLines(edit.old_string || '', edit.new_string || '', { newlineIsToken: true, ignoreWhitespace: false }); return (
Edit {index + 1}
{diffResult.map((part, partIndex) => { const partClass = part.added ? 'bg-green-950/20' : part.removed ? 'bg-red-950/20' : ''; if (!part.added && !part.removed && part.count && part.count > 8) { return (
... {part.count} unchanged lines ...
); } const value = part.value.endsWith('\n') ? part.value.slice(0, -1) : part.value; return (
{part.added ? + : part.removed ? - : null}
{value}
); })}
); })}
)}
); }; /** * Widget for displaying MultiEdit tool results with diffs */ export const MultiEditResultWidget: React.FC<{ content: string; edits?: Array<{ old_string: string; new_string: string }>; }> = ({ content, edits }) => { // If we have the edits array, show a nice diff view if (edits && edits.length > 0) { return (
{edits.length} Changes Applied
{edits.map((edit, index) => { // Split the strings into lines for diff display const oldLines = edit.old_string.split('\n'); const newLines = edit.new_string.split('\n'); return (
Change {index + 1}
{/* Show removed lines */} {oldLines.map((line, lineIndex) => (
-{lineIndex + 1}
                        {line || ' '}
                      
))} {/* Show added lines */} {newLines.map((line, lineIndex) => (
+{lineIndex + 1}
                        {line || ' '}
                      
))}
); })}
); } // Fallback to simple content display return (
{content}
); }; /** * Widget for displaying system reminders (instead of raw XML) */ export const SystemReminderWidget: React.FC<{ message: string }> = ({ message }) => { // Extract icon based on message content let icon = ; let colorClass = "border-blue-500/20 bg-blue-500/5 text-blue-600"; if (message.toLowerCase().includes("warning")) { icon = ; colorClass = "border-yellow-500/20 bg-yellow-500/5 text-yellow-600"; } else if (message.toLowerCase().includes("error")) { icon = ; colorClass = "border-destructive/20 bg-destructive/5 text-destructive"; } return (
{icon}
{message}
); }; /** * Widget for displaying system initialization information in a visually appealing way * Separates regular tools from MCP tools and provides icons for each tool type */ export const SystemInitializedWidget: React.FC<{ sessionId?: string; model?: string; cwd?: string; tools?: string[]; }> = ({ sessionId, model, cwd, tools = [] }) => { const [mcpExpanded, setMcpExpanded] = useState(false); // Separate regular tools from MCP tools const regularTools = tools.filter(tool => !tool.startsWith('mcp__')); const mcpTools = tools.filter(tool => tool.startsWith('mcp__')); // Tool icon mapping for regular tools const toolIcons: Record = { 'task': CheckSquare, 'bash': Terminal, 'glob': FolderSearch, 'grep': Search, 'ls': List, 'exit_plan_mode': LogOut, 'read': FileText, 'edit': Edit3, 'multiedit': Edit3, 'write': FilePlus, 'notebookread': Book, 'notebookedit': BookOpen, 'webfetch': Globe, 'todoread': ListChecks, 'todowrite': ListPlus, 'websearch': Globe2, }; // Get icon for a tool, fallback to Wrench const getToolIcon = (toolName: string) => { const normalizedName = toolName.toLowerCase(); return toolIcons[normalizedName] || Wrench; }; // Format MCP tool name (remove mcp__ prefix and format underscores) const formatMcpToolName = (toolName: string) => { // Remove mcp__ prefix const withoutPrefix = toolName.replace(/^mcp__/, ''); // Split by double underscores first (provider separator) const parts = withoutPrefix.split('__'); if (parts.length >= 2) { // Format provider name and method name separately const provider = parts[0].replace(/_/g, ' ').replace(/-/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); const method = parts.slice(1).join('__').replace(/_/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); return { provider, method }; } // Fallback formatting return { provider: 'MCP', method: withoutPrefix.replace(/_/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' ') }; }; // Group MCP tools by provider const mcpToolsByProvider = mcpTools.reduce((acc, tool) => { const { provider } = formatMcpToolName(tool); if (!acc[provider]) { acc[provider] = []; } acc[provider].push(tool); return acc; }, {} as Record); return (

System Initialized

{/* Session Info */}
{sessionId && (
Session ID: {sessionId}
)} {model && (
Model: {model}
)} {cwd && (
Working Directory: {cwd}
)}
{/* Regular Tools */} {regularTools.length > 0 && (
Available Tools ({regularTools.length})
{regularTools.map((tool, idx) => { const Icon = getToolIcon(tool); return ( {tool} ); })}
)} {/* MCP Tools */} {mcpTools.length > 0 && (
{mcpExpanded && (
{Object.entries(mcpToolsByProvider).map(([provider, providerTools]) => (
{provider} ({providerTools.length})
{providerTools.map((tool, idx) => { const { method } = formatMcpToolName(tool); return ( {method} ); })}
))}
)}
)} {/* Show message if no tools */} {tools.length === 0 && (
No tools available
)}
); }; /** * Widget for Task tool - displays sub-agent task information */ export const TaskWidget: React.FC<{ description?: string; prompt?: string; result?: any; }> = ({ description, prompt, result: _result }) => { const [isExpanded, setIsExpanded] = useState(false); return (
Spawning Sub-Agent Task
{description && (
Task Description

{description}

)} {prompt && (
{isExpanded && (
                  {prompt}
                
)}
)}
); }; /** * Widget for displaying AI thinking/reasoning content * Collapsible and closed by default */ export const ThinkingWidget: React.FC<{ thinking: string; signature?: string; }> = ({ thinking, signature }) => { const [isExpanded, setIsExpanded] = useState(false); return (
{isExpanded && (
              {thinking}
            
{signature && (
Signature: {signature.slice(0, 16)}...
)}
)}
); };