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);
+};