diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index 263a1c28321e..f8e4729f34b3 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -7,6 +7,12 @@ import { extractImagePaths, removeImagePathsFromText } from '../utils/imageUtils import { formatMessageTimestamp } from '../utils/timeUtils'; import MarkdownContent from './MarkdownContent'; import ToolCallWithResponse from './ToolCallWithResponse'; +import ToolCallChain from './ToolCallChain'; +import { + identifyConsecutiveToolCalls, + shouldHideMessage, + getChainForMessage, +} from '../utils/toolCallChaining'; import { Message, getTextContent, @@ -18,6 +24,7 @@ import { import ToolCallConfirmation from './ToolCallConfirmation'; import MessageCopyLink from './MessageCopyLink'; import { NotificationEvent } from '../hooks/useMessageStream'; +import { cn } from '../utils'; interface GooseMessageProps { // messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests @@ -60,18 +67,23 @@ export default function GooseMessage({ } const cotRaw = match[1].trim(); - const visible = text.replace(match[0], '').trim(); - return { visibleText: visible, cotText: cotRaw.length > 0 ? cotRaw : null }; + const visibleText = text.replace(regex, '').trim(); + + return { + visibleText, + cotText: cotRaw || null, + }; }; - const { visibleText: textWithoutCot, cotText } = splitChainOfThought(textContent); + // Split out Chain-of-Thought + const { visibleText, cotText } = splitChainOfThought(textContent); - // Extract image paths from the visible part of the message (exclude CoT) - const imagePaths = extractImagePaths(textWithoutCot); + // Extract image paths from the message content + const imagePaths = extractImagePaths(visibleText); // Remove image paths from text for display const displayText = - imagePaths.length > 0 ? removeImagePathsFromText(textWithoutCot, imagePaths) : textWithoutCot; + imagePaths.length > 0 ? removeImagePathsFromText(visibleText, imagePaths) : visibleText; // Memoize the timestamp const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]); @@ -79,11 +91,53 @@ export default function GooseMessage({ // Get tool requests from the message const toolRequests = getToolRequests(message); + // Get current message index + const messageIndex = messages.findIndex((msg) => msg.id === message.id); + + // Enhanced chain detection that works during streaming + const toolCallChains = useMemo(() => { + // Always run chain detection, but handle streaming messages specially + const chains = identifyConsecutiveToolCalls(messages); + + // If this message is streaming and has tool calls but no text, + // check if it should extend an existing chain + if (isStreaming && toolRequests.length > 0 && !displayText.trim()) { + // Look for an existing chain that this message could extend + const previousMessage = messageIndex > 0 ? messages[messageIndex - 1] : null; + if (previousMessage) { + const prevToolRequests = getToolRequests(previousMessage); + + // If previous message has tool calls (with or without text), extend its chain + if (prevToolRequests.length > 0) { + // Find if previous message is part of a chain + const prevChain = chains.find((chain) => chain.includes(messageIndex - 1)); + if (prevChain) { + // Extend the existing chain to include this streaming message + const extendedChains = chains.map((chain) => + chain === prevChain ? [...chain, messageIndex] : chain + ); + return extendedChains; + } else { + // Create a new chain with previous and current message + return [...chains, [messageIndex - 1, messageIndex]]; + } + } + } + } + + return chains; + }, [messages, isStreaming, messageIndex, toolRequests, displayText]); + + // Check if this message should be hidden (part of chain but not first) + const shouldHide = shouldHideMessage(messageIndex, toolCallChains); + + // Get the chain this message belongs to + const messageChain = getChainForMessage(messageIndex, toolCallChains); + // Extract URLs under a few conditions // 1. The message is purely text // 2. The link wasn't also present in the previous message // 3. The message contains the explicit http:// or https:// protocol at the beginning - const messageIndex = messages?.findIndex((msg) => msg.id === message.id); const previousMessage = messageIndex > 0 ? messages[messageIndex - 1] : null; const previousUrls = previousMessage ? extractUrls(getTextContent(previousMessage)) : []; const urls = toolRequests.length === 0 ? extractUrls(displayText, previousUrls) : []; @@ -145,10 +199,17 @@ export default function GooseMessage({ appendMessage, ]); + // If this message should be hidden (part of chain but not first), don't render it + if (shouldHide) { + return null; + } + + // Determine rendering logic based on chain membership and content + const isFirstInChain = messageChain && messageChain[0] === messageIndex; + return (
- {/* Chain-of-Thought (hidden by default) */} {cotText && (
@@ -160,61 +221,73 @@ export default function GooseMessage({
)} - {/* Visible assistant response */} {displayText && (
-
-
{}
+
+
- {/* Render images if any */} + {/* Image previews */} {imagePaths.length > 0 && ( -
+
{imagePaths.map((imagePath, index) => ( - + ))}
)} - {/* Only show timestamp and copy link when not streaming */} -
- {toolRequests.length === 0 && !isStreaming && ( -
- {timestamp} -
- )} - {displayText && - message.content.every((content) => content.type === 'text') && - !isStreaming && ( + {toolRequests.length === 0 && ( +
+ {!isStreaming && ( +
+ {timestamp} +
+ )} + {message.content.every((content) => content.type === 'text') && !isStreaming && (
)} -
+
+ )}
)} {toolRequests.length > 0 && ( -
- {toolRequests.map((toolRequest) => ( -
- +
+ {isFirstInChain ? ( + + ) : !messageChain ? ( +
+
+ {toolRequests.map((toolRequest) => ( +
+ +
+ ))} +
+
+ {!isStreaming && timestamp} +
- ))} -
- {!isStreaming && timestamp} -
+ ) : null}
)} @@ -229,9 +302,8 @@ export default function GooseMessage({
{/* TODO(alexhancock): Re-enable link previews once styled well again */} - {/* eslint-disable-next-line no-constant-binary-expression */} - {false && urls.length > 0 && ( -
+ {urls.length > 0 && ( +
{urls.map((url, index) => ( ))} diff --git a/ui/desktop/src/components/ToolCallChain.tsx b/ui/desktop/src/components/ToolCallChain.tsx new file mode 100644 index 000000000000..ea02e343c42f --- /dev/null +++ b/ui/desktop/src/components/ToolCallChain.tsx @@ -0,0 +1,56 @@ +import { formatMessageTimestamp } from '../utils/timeUtils'; +import { Message, getToolRequests } from '../types/message'; +import { NotificationEvent } from '../hooks/useMessageStream'; +import ToolCallWithResponse from './ToolCallWithResponse'; + +interface ToolCallChainProps { + messages: Message[]; + chainIndices: number[]; + toolCallNotifications: Map; + toolResponsesMap: Map; + messageHistoryIndex: number; + isStreaming?: boolean; +} + +export default function ToolCallChain({ + messages, + chainIndices, + toolCallNotifications, + toolResponsesMap, + messageHistoryIndex, + isStreaming = false, +}: ToolCallChainProps) { + const lastMessageIndex = chainIndices[chainIndices.length - 1]; + const lastMessage = messages[lastMessageIndex]; + const timestamp = lastMessage ? formatMessageTimestamp(lastMessage.created) : ''; + + return ( +
+
+ {chainIndices.map((messageIndex) => { + const message = messages[messageIndex]; + const toolRequests = getToolRequests(message); + + return toolRequests.map((toolRequest) => ( +
+ +
+ )); + })} +
+ +
+ {!isStreaming && timestamp} +
+
+ ); +} diff --git a/ui/desktop/src/components/ToolCallStatusIndicator.tsx b/ui/desktop/src/components/ToolCallStatusIndicator.tsx new file mode 100644 index 000000000000..7477e5fb0ec7 --- /dev/null +++ b/ui/desktop/src/components/ToolCallStatusIndicator.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { cn } from '../utils'; + +export type ToolCallStatus = 'pending' | 'loading' | 'success' | 'error'; + +interface ToolCallStatusIndicatorProps { + status: ToolCallStatus; + className?: string; +} + +export const ToolCallStatusIndicator: React.FC = ({ + status, + className, +}) => { + const getStatusStyles = () => { + switch (status) { + case 'success': + return 'bg-green-500'; + case 'error': + return 'bg-red-500'; + case 'loading': + return 'bg-yellow-500 animate-pulse'; + case 'pending': + default: + return 'bg-gray-400'; + } + }; + + return ( +
+ ); +}; + +/** + * Wrapper component that adds a status indicator to a tool icon + */ +interface ToolIconWithStatusProps { + ToolIcon: React.ComponentType<{ className?: string }>; + status: ToolCallStatus; + className?: string; +} + +export const ToolIconWithStatus: React.FC = ({ + ToolIcon, + status, + className, +}) => { + return ( +
+ + +
+ ); +}; diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index 274224f79e93..85b461be1247 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -1,12 +1,14 @@ +import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator'; +import { getToolCallIcon } from '../utils/toolIconMapping'; 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 { LoadingStatus } from './ui/Dot'; import { NotificationEvent } from '../hooks/useMessageStream'; -import { ChevronRight, FlaskConical, LoaderCircle } from 'lucide-react'; +import { ChevronRight, FlaskConical } from 'lucide-react'; import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper'; import MCPUIResourceRenderer from './MCPUIResourceRenderer'; import { isUIResource } from '@mcp-ui/client'; @@ -173,16 +175,13 @@ function ToolCallView({ }: 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 () => { @@ -283,12 +282,10 @@ function ToolCallView({ 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; }; @@ -442,34 +439,45 @@ function ToolCallView({ // Fallback tool name formatting return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2)); }; + // 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()} + + + {getToolLabelContent()} ); - return ( -
- {loadingStatus === 'loading' ? ( - - ) : ( - - )} -
- {extensionTooltip ? ( - - {toolLabel} - - ) : ( - toolLabel - )} - + extensionTooltip ? ( + + {toolLabel} + + ) : ( + toolLabel + ) } > {/* Tool Details */} diff --git a/ui/desktop/src/components/icons/toolcalls/Archive.tsx b/ui/desktop/src/components/icons/toolcalls/Archive.tsx new file mode 100644 index 000000000000..e6454c71027f --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Archive.tsx @@ -0,0 +1,23 @@ +export const Archive = ({ className }: { className?: string }) => ( + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Brain.tsx b/ui/desktop/src/components/icons/toolcalls/Brain.tsx new file mode 100644 index 000000000000..4b13da2efcdf --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Brain.tsx @@ -0,0 +1,27 @@ +export const Brain = ({ className }: { className?: string }) => ( + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Camera.tsx b/ui/desktop/src/components/icons/toolcalls/Camera.tsx new file mode 100644 index 000000000000..b05358d66a13 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Camera.tsx @@ -0,0 +1,33 @@ +export const Camera = ({ className }: { className?: string }) => ( + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Code2.tsx b/ui/desktop/src/components/icons/toolcalls/Code2.tsx new file mode 100644 index 000000000000..144eed687e84 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Code2.tsx @@ -0,0 +1,13 @@ +export const Code2 = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Eye.tsx b/ui/desktop/src/components/icons/toolcalls/Eye.tsx new file mode 100644 index 000000000000..bc530a2488f7 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Eye.tsx @@ -0,0 +1,33 @@ +export const Eye = ({ className }: { className?: string }) => ( + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/FileEdit.tsx b/ui/desktop/src/components/icons/toolcalls/FileEdit.tsx new file mode 100644 index 000000000000..4d4fc498c616 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/FileEdit.tsx @@ -0,0 +1,33 @@ +export const FileEdit = ({ className }: { className?: string }) => ( + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/FilePlus.tsx b/ui/desktop/src/components/icons/toolcalls/FilePlus.tsx new file mode 100644 index 000000000000..e34b18ed32fe --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/FilePlus.tsx @@ -0,0 +1,27 @@ +export const FilePlus = ({ className }: { className?: string }) => ( + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/FileText.tsx b/ui/desktop/src/components/icons/toolcalls/FileText.tsx new file mode 100644 index 000000000000..45e7cccc3bdb --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/FileText.tsx @@ -0,0 +1,31 @@ +export const FileText = ({ className }: { className?: string }) => ( + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Globe.tsx b/ui/desktop/src/components/icons/toolcalls/Globe.tsx new file mode 100644 index 000000000000..c3c7956a45f0 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Globe.tsx @@ -0,0 +1,34 @@ +export const Globe = ({ className }: { className?: string }) => ( + + + + + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Harddrive.tsx b/ui/desktop/src/components/icons/toolcalls/Harddrive.tsx new file mode 100644 index 000000000000..aa34ff726f24 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Harddrive.tsx @@ -0,0 +1,16 @@ +export const Harddrive = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Monitor.tsx b/ui/desktop/src/components/icons/toolcalls/Monitor.tsx new file mode 100644 index 000000000000..a36fb48fb8d1 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Monitor.tsx @@ -0,0 +1,16 @@ +export const Monitor = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Numbers.tsx b/ui/desktop/src/components/icons/toolcalls/Numbers.tsx new file mode 100644 index 000000000000..d47747f3ee8f --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Numbers.tsx @@ -0,0 +1,37 @@ +export const Numbers = ({ className }: { className?: string }) => ( + + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Save.tsx b/ui/desktop/src/components/icons/toolcalls/Save.tsx new file mode 100644 index 000000000000..d9bfc91a5a73 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Save.tsx @@ -0,0 +1,13 @@ +export const Save = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Search.tsx b/ui/desktop/src/components/icons/toolcalls/Search.tsx new file mode 100644 index 000000000000..2486622b314e --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Search.tsx @@ -0,0 +1,29 @@ +export const Search = ({ className }: { className?: string }) => ( + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Settings.tsx b/ui/desktop/src/components/icons/toolcalls/Settings.tsx new file mode 100644 index 000000000000..32ec58c77e94 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Settings.tsx @@ -0,0 +1,40 @@ +export const Settings = ({ className }: { className?: string }) => ( + + + + + + + + + + + + + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Terminal.tsx b/ui/desktop/src/components/icons/toolcalls/Terminal.tsx new file mode 100644 index 000000000000..c7a1d7507587 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Terminal.tsx @@ -0,0 +1,13 @@ +export const Terminal = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/Tool.tsx b/ui/desktop/src/components/icons/toolcalls/Tool.tsx new file mode 100644 index 000000000000..c0475ab15e7c --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/Tool.tsx @@ -0,0 +1,13 @@ +export const Tool = ({ className }: { className?: string }) => ( + + + + +); diff --git a/ui/desktop/src/components/icons/toolcalls/index.tsx b/ui/desktop/src/components/icons/toolcalls/index.tsx new file mode 100644 index 000000000000..c77ccff58941 --- /dev/null +++ b/ui/desktop/src/components/icons/toolcalls/index.tsx @@ -0,0 +1,17 @@ +export { Archive } from './Archive'; +export { Brain } from './Brain'; +export { Camera } from './Camera'; +export { Code2 } from './Code2'; +export { Eye } from './Eye'; +export { FileEdit } from './FileEdit'; +export { FilePlus } from './FilePlus'; +export { FileText } from './FileText'; +export { Globe } from './Globe'; +export { Harddrive } from './Harddrive'; +export { Monitor } from './Monitor'; +export { Numbers } from './Numbers'; +export { Save } from './Save'; +export { Search } from './Search'; +export { Settings } from './Settings'; +export { Terminal } from './Terminal'; +export { Tool } from './Tool'; diff --git a/ui/desktop/src/utils/toolCallChaining.ts b/ui/desktop/src/utils/toolCallChaining.ts new file mode 100644 index 000000000000..8a537e696198 --- /dev/null +++ b/ui/desktop/src/utils/toolCallChaining.ts @@ -0,0 +1,60 @@ +import { Message, getToolRequests, getTextContent, getToolResponses } from '../types/message'; + +export function identifyConsecutiveToolCalls(messages: Message[]): number[][] { + const chains: number[][] = []; + let currentChain: number[] = []; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + const toolRequests = getToolRequests(message); + const toolResponses = getToolResponses(message); + const textContent = getTextContent(message); + const hasText = textContent.trim().length > 0; + + if (toolResponses.length > 0 && toolRequests.length === 0) { + continue; + } + + if (toolRequests.length > 0) { + if (hasText) { + if (currentChain.length > 0) { + if (currentChain.length > 1) { + chains.push([...currentChain]); + } + } + currentChain = [i]; + } else { + currentChain.push(i); + } + } else if (hasText) { + if (currentChain.length > 1) { + chains.push([...currentChain]); + } + currentChain = []; + } else { + if (currentChain.length > 1) { + chains.push([...currentChain]); + } + currentChain = []; + } + } + + if (currentChain.length > 1) { + chains.push(currentChain); + } + + return chains; +} + +export function shouldHideMessage(messageIndex: number, chains: number[][]): boolean { + for (const chain of chains) { + if (chain.includes(messageIndex)) { + return chain[0] !== messageIndex; + } + } + return false; +} + +export function getChainForMessage(messageIndex: number, chains: number[][]): number[] | null { + return chains.find(chain => chain.includes(messageIndex)) || null; +} \ No newline at end of file diff --git a/ui/desktop/src/utils/toolIconMapping.tsx b/ui/desktop/src/utils/toolIconMapping.tsx new file mode 100644 index 000000000000..c8f456cdcc24 --- /dev/null +++ b/ui/desktop/src/utils/toolIconMapping.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { + Archive, + Brain, + Camera, + Code2, + Eye, + FileEdit, + FilePlus, + FileText, + Globe, + Monitor, + Numbers, + Save, + Search, + Settings, + Terminal, + Tool, +} from '../components/icons/toolcalls'; + +export type ToolIconProps = { + className?: string; +}; + +/** + * Maps tool names to their corresponding icon components + * @param toolName - The name of the tool (extracted from toolCall.name) + * @returns React component for the tool icon + */ +export const getToolIcon = (toolName: string): React.ComponentType => { + switch (toolName) { + // Developer Extension Tools + case 'text_editor': + return FileEdit; + case 'shell': + return Terminal; + + // Memory Extension Tools + case 'remember_memory': + return Save; + case 'retrieve_memories': + return Brain; + + // Computer Controller Extension Tools + case 'automation_script': + return Settings; + case 'computer_control': + return Monitor; + case 'web_scrape': + return Globe; + case 'screen_capture': + return Camera; + case 'pdf_tool': + return FileText; + case 'docx_tool': + return FileText; + case 'xlsx_tool': + return Numbers; + case 'cache': + return Archive; + + // File Operations + case 'search': + return Search; + case 'read': + return Eye; + case 'create_file': + return FilePlus; + case 'update_file': + return FileEdit; + + // Google Workspace Tools (if still supported) + case 'sheets_tool': + return Numbers; + case 'docs_tool': + return FileText; + + // Special Tools + case 'final_output': + return Tool; // Could be a checkmark icon if we had one + + // Default fallback for unknown tools + default: + return Tool; + } +}; + +/** + * Maps extension names to their corresponding icon components + * @param extensionName - The name of the extension + * @returns React component for the extension icon + */ +export const getExtensionIcon = (extensionName: string): React.ComponentType => { + switch (extensionName) { + case 'developer': + return Code2; + case 'memory': + return Brain; + case 'computercontroller': + return Monitor; + default: + return Tool; + } +}; + +/** + * Helper function to extract tool name from full tool call name + * @param toolCallName - Full tool call name (e.g., "developer__text_editor") + * @returns Extracted tool name (e.g., "text_editor") + */ +export const extractToolName = (toolCallName: string): string => { + return toolCallName.substring(toolCallName.lastIndexOf('__') + 2); +}; + +/** + * Helper function to extract extension name from full tool call name + * @param toolCallName - Full tool call name (e.g., "developer__text_editor") + * @returns Extracted extension name (e.g., "developer") + */ +export const extractExtensionName = (toolCallName: string): string => { + const lastIndex = toolCallName.lastIndexOf('__'); + return lastIndex === -1 ? '' : toolCallName.substring(0, lastIndex); +}; + +/** + * Main function to get the appropriate icon for a tool call + * @param toolCallName - Full tool call name (e.g., "developer__text_editor") + * @param useExtensionIcon - Whether to use extension icon instead of tool icon + * @returns React component for the icon + */ +export const getToolCallIcon = ( + toolCallName: string, + useExtensionIcon: boolean = false +): React.ComponentType => { + if (useExtensionIcon) { + const extensionName = extractExtensionName(toolCallName); + return getExtensionIcon(extensionName); + } + + const toolName = extractToolName(toolCallName); + return getToolIcon(toolName); +};