Skip to content
72 changes: 54 additions & 18 deletions ui/desktop/src/components/ToolCallWithResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -289,46 +301,70 @@ 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 (
<ToolCallExpandable
isStartExpanded={isRenderingProgress}
isForceExpand={isShouldExpand}
label={
<>
<Dot size={2} loadingStatus={loadingStatus} />
<span className="ml-[10px]">
{(() => {
const description = getToolDescription();
if (description) {
return description;
}
// Fallback to the original tool name formatting
return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2));
})()}
</span>
{extensionTooltip ? (
<TooltipWrapper tooltipContent={extensionTooltip} side="top">
<span className="ml-[10px] cursor-pointer hover:opacity-80">
{(() => {
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));
})()}
</span>
</TooltipWrapper>
) : (
<span className="ml-[10px]">
{(() => {
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));
})()}
</span>
)}
</>
}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 (
Expand Down
62 changes: 62 additions & 0 deletions ui/desktop/src/utils/localStorage.ts
Original file line number Diff line number Diff line change
@@ -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));
};
Loading