+
{key}
+
{isExpanded ? (
-
-
+
+
) : (
-
{value.slice(0, 60)}...
+
{value.slice(0, 60)}...
)}
toggleKey(key)}
- className="text-sm hover:opacity-75 text-textStandard"
+ className="hover:opacity-75 text-textPlaceholder pr-2"
>
-
+
@@ -73,8 +71,8 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
return (
-
{key}:
-
{content}
+
{key}:
+
{content}
);
diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx
index ab9146e1..327cf16d 100644
--- a/ui/desktop/src/components/ToolCallWithResponse.tsx
+++ b/ui/desktop/src/components/ToolCallWithResponse.tsx
@@ -1,12 +1,11 @@
import React from 'react';
import { Card } from './ui/card';
-import Box from './ui/Box';
import { ToolCallArguments } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent';
-import { LoadingPlaceholder } from './LoadingPlaceholder';
-import { ChevronUp } from 'lucide-react';
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
import { snakeToTitleCase } from '../utils';
+import Dot, { LoadingStatus } from './ui/Dot';
+import Expand from './ui/Expand';
interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
@@ -20,138 +19,166 @@ export default function ToolCallWithResponse({
toolResponse,
}: ToolCallWithResponseProps) {
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
-
if (!toolCall) {
return null;
}
return (
-
+
-
- {!isCancelledMessage ? (
- toolResponse ? (
-
- ) : (
-
- )
- ) : undefined}
+
);
}
+interface ToolCallExpandableProps {
+ label: string | React.ReactNode;
+ defaultExpanded?: boolean;
+ forceExpand?: boolean;
+ children: React.ReactNode;
+ className?: string;
+}
+
+function ToolCallExpandable({
+ label,
+ defaultExpanded = false,
+ forceExpand,
+ children,
+ className = '',
+}: ToolCallExpandableProps) {
+ const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
+ const toggleExpand = () => setIsExpanded((prev) => !prev);
+ React.useEffect(() => {
+ if (forceExpand) setIsExpanded(true);
+ }, [forceExpand]);
+
+ return (
+
+
+ {label}
+
+
+ {isExpanded &&
{children}
}
+
+ );
+}
+
interface ToolCallViewProps {
+ isCancelledMessage: boolean;
+ toolCall: {
+ name: string;
+ arguments: Record
;
+ };
+ toolResponse?: ToolResponseMessageContent;
+}
+
+function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallViewProps) {
+ const isToolDetails = Object.entries(toolCall?.arguments).length > 0;
+ const loadingStatus: LoadingStatus = !toolResponse?.toolResult.status
+ ? 'loading'
+ : toolResponse?.toolResult.status;
+
+ const toolResults: { result: Content; defaultExpanded: 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) => ({
+ result: item,
+ defaultExpanded: ((item.annotations?.priority as number | undefined) ?? -1) >= 0.5,
+ }))
+ : [];
+
+ const shouldExpand = toolResults.some((v) => v.defaultExpanded);
+
+ return (
+
+
+
+ {snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2))}
+
+ >
+ }
+ >
+ {/* Tool Details */}
+ {isToolDetails && (
+
+
+
+ )}
+
+ {/* Tool Output */}
+ {!isCancelledMessage && (
+ <>
+ {toolResults.map(({ result, defaultExpanded }, index) => {
+ const isLast = index === toolResults.length - 1;
+ return (
+
+
+
+ );
+ })}
+ >
+ )}
+
+ );
+}
+
+interface ToolDetailsViewProps {
toolCall: {
name: string;
arguments: Record;
};
}
-function ToolCallView({ toolCall }: ToolCallViewProps) {
+function ToolDetailsView({ toolCall }: ToolDetailsViewProps) {
return (
-
-
-
-
- {snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2))}
-
-
-
+
{toolCall.arguments && }
-
-
-
+
);
}
interface ToolResultViewProps {
- result?: Content[];
+ result: Content;
+ defaultExpanded: boolean;
}
-function ToolResultView({ result }: ToolResultViewProps) {
- // State to track expanded items
- const [expandedItems, setExpandedItems] = React.useState([]);
-
- // If no result info, don't show anything
- if (!result) return null;
-
- // Find results where either audience is not set, or it's set to a list that includes user
- const filteredResults = result.filter((item) => {
- // Check audience (which may not be in the type)
- const audience = item.annotations?.audience;
-
- return !audience || audience.includes('user');
- });
-
- if (filteredResults.length === 0) return null;
-
- const toggleExpand = (index: number) => {
- setExpandedItems((prev) =>
- prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
- );
- };
-
- const shouldShowExpanded = (item: Content, index: number) => {
- return (
- (item.annotations &&
- item.annotations.priority !== undefined &&
- item.annotations.priority >= 0.5) ||
- expandedItems.includes(index)
- );
- };
-
+function ToolResultView({ result, defaultExpanded }: ToolResultViewProps) {
return (
-
- {filteredResults.map((item, index) => {
- const isExpanded = shouldShowExpanded(item, index);
- const shouldMinimize =
- !item.annotations ||
- item.annotations.priority === undefined ||
- item.annotations.priority < 0.5;
- return (
-
- {shouldMinimize && (
-
toggleExpand(index)}
- className="mb-1 flex items-center text-textStandard"
- >
- Output
-
-
- )}
- {(isExpanded || !shouldMinimize) && (
- <>
- {item.type === 'text' && item.text && (
-
- )}
- {item.type === 'image' && (
-
{
- console.error('Failed to load image: Invalid MIME-type encoded image data');
- e.currentTarget.style.display = 'none';
- }}
- />
- )}
- >
- )}
-
- );
- })}
-
+ Output}
+ defaultExpanded={defaultExpanded}
+ >
+
+ {result.type === 'text' && result.text && (
+
+ )}
+ {result.type === 'image' && (
+
{
+ console.error('Failed to load image');
+ e.currentTarget.style.display = 'none';
+ }}
+ />
+ )}
+
+
);
}
diff --git a/ui/desktop/src/components/ui/Dot.tsx b/ui/desktop/src/components/ui/Dot.tsx
new file mode 100644
index 00000000..c6583b8e
--- /dev/null
+++ b/ui/desktop/src/components/ui/Dot.tsx
@@ -0,0 +1,24 @@
+export type LoadingStatus = 'loading' | 'success' | 'error';
+export default function Dot({
+ size,
+ loadingStatus,
+}: {
+ size: number;
+ loadingStatus: LoadingStatus;
+}) {
+ const backgroundColor =
+ {
+ loading: '#2693FF',
+ success: 'var(--icon-extra-subtle)',
+ error: '#CC0023',
+ }[loadingStatus] ?? 'var(--icon-extra-subtle)';
+
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/ui/Expand.tsx b/ui/desktop/src/components/ui/Expand.tsx
new file mode 100644
index 00000000..0529248b
--- /dev/null
+++ b/ui/desktop/src/components/ui/Expand.tsx
@@ -0,0 +1,9 @@
+import { ChevronUp } from 'lucide-react';
+
+export default function Expand({ size, isExpanded }: { size: number; isExpanded: boolean }) {
+ return (
+
+ );
+}