diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index 971bfea58f5b..9686904157b6 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -7,6 +7,7 @@ import { snakeToTitleCase } from '../utils'; import Dot, { LoadingStatus } from './ui/Dot'; import Expand from './ui/Expand'; import { NotificationEvent } from '../hooks/useMessageStream'; +import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper'; interface ToolCallWithResponseProps { isCancelledMessage: boolean; @@ -105,6 +106,17 @@ const logToString = (logMessage: NotificationEvent) => { 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, @@ -277,7 +289,7 @@ function ToolCallView({ if (args.window_title) { return `capturing window "${truncate(getStringValue(args.window_title))}"`; } - return 'capturing screen'; + return `capturing screen`; case 'automation_script': if (args.language) { @@ -289,29 +301,38 @@ function ToolCallView({ return 'final output'; case 'computer_control': - return 'poking around...'; + return `poking around...`; default: { - // Fallback to showing key parameters for unknown tools + // Fallback to the old generic approach: 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 null; + + if (entries.length === 0) { + return `${toolDisplayName}`; + } - // For a single parameter, show key and truncated value + // For a single parameter, show key and truncated value (like the old system) if (entries.length === 1) { const [key, value] = entries[0]; const stringValue = getStringValue(value); const truncatedValue = truncate(stringValue, 30); - return `${key}: ${truncatedValue}`; + return `${toolDisplayName} ${key}: ${truncatedValue}`; } - // For multiple parameters, just show the keys - return entries.map(([key]) => key).join(', '); + // 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); + return ( - - {(() => { - const description = getToolDescription(); - if (description) { - return description; - } - // Fallback to the original tool name formatting - return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2)); - })()} - + {extensionTooltip ? ( + + + {(() => { + const description = getToolDescription(); + if (description) { + return description; + } + // Fallback to the original tool name formatting without extension prefix + return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2)); + })()} + + + ) : ( + + {(() => { + const description = getToolDescription(); + if (description) { + return description; + } + // Fallback to the original tool name formatting without extension prefix + return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2)); + })()} + + )} } > diff --git a/ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx b/ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx index 5cf18bb7cd8b..9e82c8377da0 100644 --- a/ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx +++ b/ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react'; import { all_response_styles, ResponseStyleSelectionItem } from './ResponseStyleSelectionItem'; +import { getLocalStorageItem, setLocalStorageItem } from '../../../utils/localStorage'; export const ResponseStylesSection = () => { const [currentStyle, setCurrentStyle] = useState('concise'); useEffect(() => { - const savedStyle = localStorage.getItem('response_style'); + const savedStyle = getLocalStorageItem('response_style', 'concise'); if (savedStyle) { try { setCurrentStyle(savedStyle); @@ -14,14 +15,14 @@ export const ResponseStylesSection = () => { } } else { // Set default to concise for new users - localStorage.setItem('response_style', 'concise'); + setLocalStorageItem('response_style', 'concise'); setCurrentStyle('concise'); } }, []); const handleStyleChange = async (newStyle: string) => { setCurrentStyle(newStyle); - localStorage.setItem('response_style', newStyle); + setLocalStorageItem('response_style', newStyle); }; return ( diff --git a/ui/desktop/src/utils/localStorage.ts b/ui/desktop/src/utils/localStorage.ts new file mode 100644 index 000000000000..511d68b4b9a0 --- /dev/null +++ b/ui/desktop/src/utils/localStorage.ts @@ -0,0 +1,62 @@ +/** + * Safe localStorage utilities that handle cases where localStorage is unavailable + * (e.g., SSR, Storybook, unit tests, or browser restrictions) + */ + +/** + * Safely get an item from localStorage with a default fallback + * @param key - The localStorage key to read + * @param defaultValue - Value to return if localStorage is unavailable or key doesn't exist + * @returns The stored value or the default value + */ +export const getLocalStorageItem = (key: string, defaultValue: string = ''): string => { + try { + if (typeof localStorage === 'undefined') { + return defaultValue; + } + return localStorage.getItem(key) ?? defaultValue; + } catch (error) { + console.warn(`Failed to read from localStorage key "${key}":`, error); + return defaultValue; + } +}; + +/** + * Safely set an item in localStorage + * @param key - The localStorage key to write + * @param value - The value to store + * @returns true if successful, false if localStorage is unavailable or write failed + */ +export const setLocalStorageItem = (key: string, value: string): boolean => { + try { + if (typeof localStorage === 'undefined') { + return false; + } + localStorage.setItem(key, value); + return true; + } catch (error) { + console.warn(`Failed to write to localStorage key "${key}":`, error); + return false; + } +}; + +/** + * Safely get a boolean value from localStorage + * @param key - The localStorage key to read + * @param defaultValue - Default boolean value if key doesn't exist or localStorage unavailable + * @returns The stored boolean value or the default + */ +export const getLocalStorageBoolean = (key: string, defaultValue: boolean = false): boolean => { + const value = getLocalStorageItem(key, String(defaultValue)); + return value === 'true'; +}; + +/** + * Safely set a boolean value in localStorage + * @param key - The localStorage key to write + * @param value - The boolean value to store + * @returns true if successful, false if localStorage is unavailable or write failed + */ +export const setLocalStorageBoolean = (key: string, value: boolean): boolean => { + return setLocalStorageItem(key, String(value)); +};