import React, { useEffect, useRef, useState } from 'react';
import { Button } from './ui/button';
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent';
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
import { cn, snakeToTitleCase } from '../utils';
import Dot, { LoadingStatus } from './ui/Dot';
import { NotificationEvent } from '../hooks/useMessageStream';
import { ChevronRight, LoaderCircle } from 'lucide-react';
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
toolRequest: ToolRequestMessageContent;
toolResponse?: ToolResponseMessageContent;
notifications?: NotificationEvent[];
isStreamingMessage?: boolean;
}
export default function ToolCallWithResponse({
isCancelledMessage,
toolRequest,
toolResponse,
notifications,
isStreamingMessage = false,
}: ToolCallWithResponseProps) {
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
if (!toolCall) {
return null;
}
return (
);
}
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 params = logMessage.message.params;
// 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 =>
notification.message.params as unknown as Progress;
// 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'));
// Listen for localStorage changes to update the response style
useEffect(() => {
const handleStorageChange = () => {
setResponseStyle(localStorage.getItem('response_style'));
};
// Listen for storage events (changes from other tabs/windows)
window.addEventListener('storage', handleStorageChange);
// Listen for custom events (changes from same tab)
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 = 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.status;
// 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: { result: Content; isExpandToolResults: boolean }[] =
loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value)
? toolResponse!.toolResult.value
.filter((item) => {
const audience = item.annotations?.audience as string[] | undefined;
return !audience || audience.includes('user');
})
.map((item) => {
// Use user preference for detailed/concise, but still respect high priority items
const priority = (item.annotations?.priority as number | undefined) ?? -1;
const isHighPriority = priority >= 0.5;
const shouldExpandBasedOnStyle = responseStyle === 'detailed' || responseStyle === null;
return {
result: item,
isExpandToolResults: isHighPriority || shouldExpandBasedOnStyle,
};
})
: [];
const logs = notifications
?.filter((notification) => notification.message.method === 'notifications/message')
.map(logToString);
const progress = notifications
?.filter((notification) => notification.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);
// Determine if the main tool call should be expanded
const isShouldExpand = (() => {
// Always expand if there are high priority results that need to be shown
const hasHighPriorityResults = toolResults.some((v) => v.isExpandToolResults);
// Also expand based on user preference for detailed mode
const shouldExpandBasedOnStyle = responseStyle === 'detailed' || responseStyle === null;
return hasHighPriorityResults || shouldExpandBasedOnStyle;
})();
// Function to create a descriptive representation of what the tool is doing
const getToolDescription = (): string | null => {
const args = toolCall.arguments as Record;
const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2);
// Helper function to get string value safely
const getStringValue = (value: ToolCallArgumentValue): string => {
return typeof value === 'string' ? value : JSON.stringify(value);
};
// Helper function to truncate long values
const truncate = (str: string, maxLength: number = 50): string => {
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
};
// Generate descriptive text based on tool type
switch (toolName) {
case 'text_editor':
if (args.command === 'write' && args.path) {
return `writing ${truncate(getStringValue(args.path))}`;
}
if (args.command === 'view' && args.path) {
return `reading ${truncate(getStringValue(args.path))}`;
}
if (args.command === 'str_replace' && args.path) {
return `editing ${truncate(getStringValue(args.path))}`;
}
if (args.command && args.path) {
return `${getStringValue(args.command)} ${truncate(getStringValue(args.path))}`;
}
break;
case 'shell':
if (args.command) {
return `running ${truncate(getStringValue(args.command))}`;
}
break;
case 'search':
if (args.name) {
return `searching for "${truncate(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 ${truncate(fileId)}`;
}
if (args.url) {
return `reading ${truncate(getStringValue(args.url))}`;
}
break;
}
case 'create_file':
if (args.name) {
return `creating ${truncate(getStringValue(args.name))}`;
}
break;
case 'update_file':
if (args.fileId) {
return `updating file ${truncate(getStringValue(args.fileId))}`;
}
break;
case 'sheets_tool': {
if (args.operation && args.spreadsheetId) {
const operation = getStringValue(args.operation);
const sheetId = truncate(getStringValue(args.spreadsheetId));
return `${operation} in sheet ${sheetId}`;
}
break;
}
case 'docs_tool': {
if (args.operation && args.documentId) {
const operation = getStringValue(args.operation);
const docId = truncate(getStringValue(args.documentId));
return `${operation} in document ${docId}`;
}
break;
}
case 'web_scrape':
if (args.url) {
return `scraping ${truncate(getStringValue(args.url))}`;
}
break;
case 'remember_memory':
if (args.category && args.data) {
return `storing ${getStringValue(args.category)}: ${truncate(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 "${truncate(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...`;
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);
const truncatedValue = truncate(stringValue, 30);
return `${toolDisplayName} ${key}: ${truncatedValue}`;
}
// 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(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2));
};
const toolLabel = (
{getToolLabelContent()}
);
return (
{loadingStatus === 'loading' ? (
) : (
)}
{extensionTooltip ? (
{toolLabel}
) : (
toolLabel
)}
>
}
>
{/* Tool Details */}
{isToolDetails && (
)}
{logs && logs.length > 0 && (
)}
{toolResults.length === 0 &&
progressEntries.length > 0 &&
progressEntries.map((entry, index) => (
))}
{/* Tool Output */}
{!isCancelledMessage && (
<>
{toolResults.map(({ result, isExpandToolResults }, index) => {
return (
);
})}
>
)}
);
}
interface ToolDetailsViewProps {
toolCall: {
name: string;
arguments: Record;
};
isStartExpanded: boolean;
}
function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
return (
Tool Details}
isStartExpanded={isStartExpanded}
>
{toolCall.arguments && (
} />
)}
);
}
interface ToolResultViewProps {
result: Content;
isStartExpanded: boolean;
}
function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
return (
Output}
isStartExpanded={isStartExpanded}
>
{result.type === 'text' && result.text && (
)}
{result.type === 'image' && (

{
console.error('Failed to load image');
e.currentTarget.style.display = 'none';
}}
/>
)}
);
}
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]);
return (
Logs
{working && (
)}
}
isStartExpanded={isStartExpanded}
>
{logs.map((log, i) => (
{log}
))}
);
}
const ProgressBar = ({ progress, total, message }: Omit