import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator'; import { getToolCallIcon } from '../utils/toolIconMapping'; import React, { useEffect, useRef, useState, useMemo } from 'react'; import { Button } from './ui/button'; import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments'; import MarkdownContent from './MarkdownContent'; import { ToolRequestMessageContent, ToolResponseMessageContent, NotificationEvent, } from '../types/message'; import { cn, snakeToTitleCase } from '../utils'; import { LoadingStatus } from './ui/Dot'; import { ChevronRight, FlaskConical } from 'lucide-react'; import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper'; import MCPUIResourceRenderer from './MCPUIResourceRenderer'; import { isUIResource } from '@mcp-ui/client'; import { CallToolResponse, Content, EmbeddedResource } from '../api'; import McpAppRenderer from './McpApps/McpAppRenderer'; interface ToolGraphNode { tool: string; description: string; depends_on: number[]; } type UiMeta = { ui?: { resourceUri?: string; }; }; type ToolResultWithMeta = { status?: string; value?: CallToolResponse & { _meta?: UiMeta; }; }; type ToolRequestWithMeta = ToolRequestMessageContent & { _meta?: UiMeta; toolCall: { status: 'success'; value: { name: string; arguments?: Record; }; }; }; interface ToolCallWithResponseProps { sessionId?: string; isCancelledMessage: boolean; toolRequest: ToolRequestMessageContent; toolResponse?: ToolResponseMessageContent; notifications?: NotificationEvent[]; isStreamingMessage?: boolean; isPendingApproval: boolean; append?: (value: string) => void; } function getToolResultContent(toolResult: Record): Content[] { if (toolResult.status !== 'success') { return []; } const value = toolResult.value as CallToolResponse; return value.content.filter((item) => { const annotations = (item as { annotations?: { audience?: string[] } }).annotations; return !annotations?.audience || annotations.audience.includes('user'); }); } function isEmbeddedResource(content: Content): content is EmbeddedResource { return 'resource' in content && typeof (content as Record).resource === 'object'; } interface McpAppWrapperProps { toolRequest: ToolRequestMessageContent; toolResponse?: ToolResponseMessageContent; sessionId: string; append?: (value: string) => void; } function McpAppWrapper({ toolRequest, toolResponse, sessionId, append, }: McpAppWrapperProps): React.ReactNode { const requestWithMeta = toolRequest as ToolRequestWithMeta; let resourceUri = requestWithMeta._meta?.ui?.resourceUri; if (!resourceUri && toolResponse) { const resultWithMeta = toolResponse.toolResult as ToolResultWithMeta; if (resultWithMeta?.status === 'success' && resultWithMeta.value) { resourceUri = resultWithMeta.value._meta?.ui?.resourceUri; } } // Tool names are formatted as "{extension_name}__{tool_name}". // Extension names can contain underscores (special chars like parentheses are normalized to "_"), // so we must use lastIndexOf to find the delimiter. // e.g., "my_server(local)" -> "my_server_local_" -> "my_server_local___get_time" const toolCallName = requestWithMeta.toolCall.status === 'success' ? requestWithMeta.toolCall.value.name : ''; const delimiterIndex = toolCallName.lastIndexOf('__'); const extensionName = delimiterIndex === -1 ? '' : toolCallName.substring(0, delimiterIndex); const toolArguments = requestWithMeta.toolCall.status === 'success' ? requestWithMeta.toolCall.value.arguments : undefined; const toolInput = useMemo(() => ({ arguments: toolArguments || {} }), [toolArguments]); const toolResult = useMemo(() => { if (!toolResponse) return undefined; const resultWithMeta = toolResponse.toolResult as ToolResultWithMeta; if (resultWithMeta?.status === 'success' && resultWithMeta.value) { return resultWithMeta.value; } return undefined; }, [toolResponse]); if (!resourceUri) return null; if (requestWithMeta.toolCall.status !== 'success') return null; return (
MCP Apps are experimental and may change at any time.
); } export default function ToolCallWithResponse({ sessionId, isCancelledMessage, toolRequest, toolResponse, notifications, isStreamingMessage, isPendingApproval, append, }: ToolCallWithResponseProps) { // Handle both the wrapped ToolResult format and the unwrapped format // The server serializes ToolResult as { status: "success", value: T } or { status: "error", error: string } const toolCallData = toolRequest.toolCall as Record; const toolCall = toolCallData?.status === 'success' ? (toolCallData.value as { name: string; arguments: Record }) : (toolCallData as { name: string; arguments: Record }); if (!toolCall || !toolCall.name) { return null; } const requestWithMeta = toolRequest as ToolRequestWithMeta; const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta; const hasMcpAppResourceURI = Boolean( requestWithMeta._meta?.ui?.resourceUri || resultWithMeta?.value?._meta?.ui?.resourceUri ); const shouldShowMcpContent = !isPendingApproval; return ( <>
{/* MCP UI — Inline */} {shouldShowMcpContent && !hasMcpAppResourceURI && toolResponse?.toolResult && getToolResultContent(toolResponse.toolResult).map((content, index) => { const resourceContent = isEmbeddedResource(content) ? { ...content, type: 'resource' as const } : null; if (resourceContent && isUIResource(resourceContent)) { return (
MCP UI is experimental and may change at any time.
); } else { return null; } })} {/* MCP App */} {shouldShowMcpContent && hasMcpAppResourceURI && sessionId && ( )} ); } interface ToolCallExpandableProps { label: string | React.ReactNode; isStartExpanded?: boolean; isForceExpand?: boolean; children: React.ReactNode; className?: string; } function ToolCallExpandable({ label, isStartExpanded = false, isForceExpand, children, className = '', }: ToolCallExpandableProps) { const [isExpandedState, setIsExpanded] = React.useState(null); const isExpanded = isExpandedState === null ? isStartExpanded : isExpandedState; const toggleExpand = () => setIsExpanded(!isExpanded); React.useEffect(() => { if (isForceExpand) setIsExpanded(true); }, [isForceExpand]); return (
{isExpanded &&
{children}
}
); } interface ToolCallViewProps { isCancelledMessage: boolean; toolCall: { name: string; arguments: Record; }; toolResponse?: ToolResponseMessageContent; notifications?: NotificationEvent[]; isStreamingMessage?: boolean; } interface Progress { progress: number; progressToken: string; total?: number; message?: string; } const logToString = (logMessage: NotificationEvent) => { const message = logMessage.message as { method: string; params: unknown }; const params = message.params as Record; // Special case for the developer system shell logs if ( params && params.data && typeof params.data === 'object' && 'output' in params.data && 'stream' in params.data ) { return `[${params.data.stream}] ${params.data.output}`; } return typeof params.data === 'string' ? params.data : JSON.stringify(params.data); }; const notificationToProgress = (notification: NotificationEvent): Progress => { const message = notification.message as { method: string; params: unknown }; return message.params as Progress; }; // Helper function to extract toolcall name const getToolName = (toolCallName: string): string => { const lastIndex = toolCallName.lastIndexOf('__'); if (lastIndex === -1) return toolCallName; return toolCallName.substring(lastIndex + 2); }; // Helper function to extract extension name for tooltip const getExtensionTooltip = (toolCallName: string): string | null => { const lastIndex = toolCallName.lastIndexOf('__'); if (lastIndex === -1) return null; const extensionName = toolCallName.substring(0, lastIndex); if (!extensionName) return null; return `${extensionName} extension`; }; function ToolCallView({ isCancelledMessage, toolCall, toolResponse, notifications, isStreamingMessage = false, }: ToolCallViewProps) { const [responseStyle, setResponseStyle] = useState(() => localStorage.getItem('response_style')); useEffect(() => { const handleStorageChange = () => { setResponseStyle(localStorage.getItem('response_style')); }; window.addEventListener('storage', handleStorageChange); window.addEventListener('responseStyleChanged', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); window.removeEventListener('responseStyleChanged', handleStorageChange); }; }, []); const isExpandToolDetails = (() => { switch (responseStyle) { case 'concise': return false; case 'detailed': default: return true; } })(); const isToolDetails = toolCall?.arguments && Object.entries(toolCall.arguments).length > 0; // Check if streaming has finished but no tool response was received // This is a workaround for cases where the backend doesn't send tool responses const isStreamingComplete = !isStreamingMessage; const shouldShowAsComplete = isStreamingComplete && !toolResponse; const loadingStatus: LoadingStatus = !toolResponse ? shouldShowAsComplete ? 'success' : 'loading' : (toolResponse.toolResult as Record).status === 'error' ? 'error' : 'success'; // Tool call timing tracking const [startTime, setStartTime] = useState(null); // Track when tool call starts (when there's no response yet) useEffect(() => { if (!toolResponse && startTime === null) { setStartTime(Date.now()); } }, [toolResponse, startTime]); const toolResults = loadingStatus === 'success' && toolResponse?.toolResult ? getToolResultContent(toolResponse.toolResult) : []; const logs = notifications ?.filter((notification) => { const message = notification.message as { method?: string }; return message.method === 'notifications/message'; }) .map(logToString); const progress = notifications ?.filter((notification) => { const message = notification.message as { method?: string }; return message.method === 'notifications/progress'; }) .map(notificationToProgress) .reduce((map, item) => { const key = item.progressToken; if (!map.has(key)) { map.set(key, []); } map.get(key)!.push(item); return map; }, new Map()); const progressEntries = [...(progress?.values() || [])].map( (entries) => entries.sort((a, b) => b.progress - a.progress)[0] ); const isRenderingProgress = loadingStatus === 'loading' && (progressEntries.length > 0 || (logs || []).length > 0); // Function to create a descriptive representation of what the tool is doing const getToolDescription = (): string | null => { const args = toolCall.arguments as Record; const toolName = getToolName(toolCall.name); const getStringValue = (value: ToolCallArgumentValue): string => { return typeof value === 'string' ? value : JSON.stringify(value); }; // Generate descriptive text based on tool type switch (toolName) { case 'text_editor': if (args.command === 'write' && args.path) { return `writing ${getStringValue(args.path)}`; } if (args.command === 'view' && args.path) { return `reading ${getStringValue(args.path)}`; } if (args.command === 'str_replace' && args.path) { return `editing ${getStringValue(args.path)}`; } if (args.command && args.path) { return `${getStringValue(args.command)} ${getStringValue(args.path)}`; } break; case 'shell': if (args.command) { return `running ${getStringValue(args.command)}`; } break; case 'search': if (args.name) { return `searching for "${getStringValue(args.name)}"`; } if (args.mimeType) { return `searching for ${getStringValue(args.mimeType)} files`; } break; case 'read': { if (args.uri) { const uri = getStringValue(args.uri); const fileId = uri.replace('gdrive:///', ''); return `reading file ${fileId}`; } if (args.url) { return `reading ${getStringValue(args.url)}`; } break; } case 'create_file': if (args.name) { return `creating ${getStringValue(args.name)}`; } break; case 'update_file': if (args.fileId) { return `updating file ${getStringValue(args.fileId)}`; } break; case 'sheets_tool': { if (args.operation && args.spreadsheetId) { const operation = getStringValue(args.operation); const sheetId = getStringValue(args.spreadsheetId); return `${operation} in sheet ${sheetId}`; } break; } case 'docs_tool': { if (args.operation && args.documentId) { const operation = getStringValue(args.operation); const docId = getStringValue(args.documentId); return `${operation} in document ${docId}`; } break; } case 'web_scrape': if (args.url) { return `scraping ${getStringValue(args.url)}`; } break; case 'remember_memory': if (args.category && args.data) { return `storing ${getStringValue(args.category)}: ${getStringValue(args.data)}`; } break; case 'retrieve_memories': if (args.category) { return `retrieving ${getStringValue(args.category)} memories`; } break; case 'screen_capture': if (args.window_title) { return `capturing window "${getStringValue(args.window_title)}"`; } return `capturing screen`; case 'automation_script': if (args.language) { return `running ${getStringValue(args.language)} script`; } break; case 'final_output': return 'final output'; case 'computer_control': return `poking around...`; case 'execute_code': { const toolGraph = args.tool_graph as unknown as ToolGraphNode[] | undefined; if (toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0) { if (toolGraph.length === 1) { return `${toolGraph[0].description}`; } if (toolGraph.length === 2) { return `${toolGraph[0].tool}, ${toolGraph[1].tool}`; } return `${toolGraph.length} tools used`; } return 'executing code'; } default: { // Generic fallback for unknown tools: ToolName + CompactArguments // This ensures any MCP tool works without explicit handling const toolDisplayName = snakeToTitleCase(toolName); const entries = Object.entries(args); if (entries.length === 0) { return `${toolDisplayName}`; } // For a single parameter, show key and truncated value if (entries.length === 1) { const [key, value] = entries[0]; const stringValue = getStringValue(value); return `${toolDisplayName} ${key}: ${stringValue}`; } // For multiple parameters, show tool name and keys const keys = entries.map(([key]) => key).join(', '); return `${toolDisplayName} ${keys}`; } } return null; }; // Get extension tooltip for the current tool const extensionTooltip = getExtensionTooltip(toolCall.name); // Extract tool label content to avoid duplication const getToolLabelContent = () => { const description = getToolDescription(); if (description) { return description; } // Fallback tool name formatting return snakeToTitleCase(getToolName(toolCall.name)); }; // Map LoadingStatus to ToolCallStatus const getToolCallStatus = (loadingStatus: LoadingStatus): ToolCallStatus => { switch (loadingStatus) { case 'success': return 'success'; case 'error': return 'error'; case 'loading': return 'loading'; default: return 'pending'; } }; const toolCallStatus = getToolCallStatus(loadingStatus); const toolLabel = ( {getToolLabelContent()} ); return ( {toolLabel} ) : ( toolLabel ) } > {(() => { const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2); const toolGraph = toolCall.arguments?.tool_graph as unknown as ToolGraphNode[] | undefined; const code = toolCall.arguments?.code as unknown as string | undefined; const hasToolGraph = toolName === 'execute_code' && toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0; if (hasToolGraph) { return (
); } if (isToolDetails) { return (
); } return null; })()} {logs && logs.length > 0 && (
)} {toolResults.length === 0 && progressEntries.length > 0 && progressEntries.map((entry, index) => (
))} {/* Tool Output */} {!isCancelledMessage && ( <> {toolResults.map((result, index) => (
))} )}
); } interface ToolDetailsViewProps { toolCall: { name: string; arguments: Record; }; isStartExpanded: boolean; } function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) { return ( Tool Details} isStartExpanded={isStartExpanded} >
{toolCall.arguments && ( } /> )}
); } interface ToolGraphViewProps { toolGraph: ToolGraphNode[]; code?: string; } function ToolGraphView({ toolGraph, code }: ToolGraphViewProps) { const renderGraph = () => { if (toolGraph.length === 0) return null; const lines: string[] = []; toolGraph.forEach((node, index) => { const deps = node.depends_on.length > 0 ? ` (uses ${node.depends_on.map((d) => d + 1).join(', ')})` : ''; lines.push(`${index + 1}. ${node.tool}: ${node.description}${deps}`); }); return lines.join('\n'); }; return (
{renderGraph()}
{code && (
Code} isStartExpanded={false} >
              {code}
            
)}
); } interface ToolResultViewProps { result: Content; isStartExpanded: boolean; } function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) { const hasText = (c: Content): c is Content & { text: string } => 'text' in c && typeof (c as Record).text === 'string'; const hasImage = (c: Content): c is Content & { data: string; mimeType: string } => { if (!('data' in c && 'mimeType' in c)) return false; const mimeType = (c as Record).mimeType; return typeof mimeType === 'string' && mimeType.startsWith('image'); }; const hasResource = (c: Content): c is Content & { resource: unknown } => 'resource' in c; return ( Output} isStartExpanded={isStartExpanded} >
{hasText(result) && ( )} {hasImage(result) && ( Tool result { console.error('Failed to load image'); e.currentTarget.style.display = 'none'; }} /> )} {hasResource(result) && (
{JSON.stringify(result, null, 2)}
)}
); } function ToolLogsView({ logs, working, isStartExpanded, }: { logs: string[]; working: boolean; isStartExpanded?: boolean; }) { const boxRef = useRef(null); // Whenever logs update, jump to the newest entry useEffect(() => { if (boxRef.current) { boxRef.current.scrollTop = boxRef.current.scrollHeight; } }, [logs.length]); // normally we do not want to put .length on an array in react deps: // // if the objects inside the array change but length doesn't change you want updates // // in this case, this is array of strings which once added do not change so this cuts // down on the possibility of unwanted runs return ( Logs {working && (
)} } isStartExpanded={isStartExpanded} >
{logs.map((log, i) => ( {log} ))}
); } const ProgressBar = ({ progress, total, message }: Omit) => { const isDeterminate = typeof total === 'number'; const percent = isDeterminate ? Math.min((progress / total!) * 100, 100) : 0; return (
{message &&
{message}
}
{isDeterminate ? (
) : (
)}
); };