diff --git a/src/components/tools/FileEditToolCall.tsx b/src/components/tools/FileEditToolCall.tsx index 75011a767..724aa6b7d 100644 --- a/src/components/tools/FileEditToolCall.tsx +++ b/src/components/tools/FileEditToolCall.tsx @@ -102,10 +102,18 @@ export const FileEditToolCall: React.FC = ({ const isWriteDenied = result && !result.success && result.error?.startsWith(WRITE_DENIED_PREFIX); const initialExpanded = !isWriteDenied; - const { expanded, toggleExpanded } = useToolExpansion(initialExpanded); + const { expanded, setExpanded, toggleExpanded } = useToolExpansion(initialExpanded); const [showRaw, setShowRaw] = React.useState(false); const [copied, setCopied] = React.useState(false); + // Update expanded state when result changes from undefined to WRITE DENIED + // This handles the case where the component renders before the result is available + React.useEffect(() => { + if (result && !result.success && result.error?.startsWith(WRITE_DENIED_PREFIX)) { + setExpanded(false); + } + }, [result, setExpanded]); + const filePath = "file_path" in args ? args.file_path : undefined; const handleCopyPatch = async () => { diff --git a/src/components/tools/GenericToolCall.tsx b/src/components/tools/GenericToolCall.tsx index bf3831930..f4c3ed4fe 100644 --- a/src/components/tools/GenericToolCall.tsx +++ b/src/components/tools/GenericToolCall.tsx @@ -33,6 +33,18 @@ export const GenericToolCall: React.FC = ({ }) => { const { expanded, toggleExpanded } = useToolExpansion(); + // Check if result contains an error + // Handles two formats: + // 1. Tool implementation errors: { success: false, error: "..." } + // 2. AI SDK tool-error events: { error: "..." } + const hasError = + result && + typeof result === "object" && + "error" in result && + typeof result.error === "string" && + result.error.length > 0 && + (!("success" in result) || result.success === false); + const hasDetails = args !== undefined || result !== undefined; return ( @@ -52,7 +64,16 @@ export const GenericToolCall: React.FC = ({ )} - {result !== undefined && ( + {hasError ? ( + + Error +
+ {String((result as { error: string }).error)} +
+
+ ) : null} + + {result !== undefined && !hasError && ( Result {formatValue(result)} diff --git a/src/utils/messages/StreamingMessageAggregator.ts b/src/utils/messages/StreamingMessageAggregator.ts index b7ea83a1d..d6b36db4a 100644 --- a/src/utils/messages/StreamingMessageAggregator.ts +++ b/src/utils/messages/StreamingMessageAggregator.ts @@ -673,8 +673,22 @@ export class StreamingMessageAggregator { timestamp: part.timestamp ?? baseTimestamp, }); } else if (isDynamicToolPart(part)) { - const status = - part.state === "output-available" + // Check if output contains an error + // Handles two formats: + // 1. Tool implementation errors: { success: false, error: "..." } + // 2. AI SDK tool-error events: { error: "..." } + const hasError = + part.state === "output-available" && + part.output && + typeof part.output === "object" && + "error" in part.output && + typeof part.output.error === "string" && + part.output.error.length > 0 && + (!("success" in part.output) || part.output.success === false); + + const status = hasError + ? "failed" + : part.state === "output-available" ? "completed" : part.state === "input-available" && message.metadata?.partial ? "interrupted"