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 && (
)}
);
};
/**
* 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 && (
)}
);
};
/**
* 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 && (
)}
{/* 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 && (
)}
$ {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 */}
{hasInput && (
~{inputTokens} tokens
{isLargeInput && (
)}
)}
{/* Tool Path */}
MCP
{formatNamespace(namespace)}
{formatMethod(method)}
()
{/* Input Parameters */}
{hasInput && (
{/* 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 (
);
};
/**
* 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 (
);
};
/**
* 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 (
{description && (
Task Description
{description}
)}
{prompt && (
{isExpanded && (
)}
)}
);
};
/**
* 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 && (
{signature && (
Signature: {signature.slice(0, 16)}...
)}
)}
);
};