Skip to content

Commit 0955e5a

Browse files
committed
🤖 Fix tool error display and WRITE DENIED collapse
Two fixes: 1. Tool error display: GenericToolCall now properly detects and displays tool errors in both formats ({ error: '...' } from AI SDK and { success: false, error: '...' } from tool implementations). Status is correctly shown as 'failed' instead of 'completed' for errors. 2. WRITE DENIED collapse: FileEditToolCall now uses useEffect to collapse when WRITE DENIED result arrives, fixing issue where initial render (before result available) would expand the tool call. Both formats are now consistently handled across all tool types.
1 parent cb523ed commit 0955e5a

File tree

3 files changed

+34
-3
lines changed

3 files changed

+34
-3
lines changed

src/components/tools/FileEditToolCall.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,17 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({
103103
const isWriteDenied = result && !result.success && result.error?.startsWith(WRITE_DENIED_PREFIX);
104104
const initialExpanded = !isWriteDenied;
105105

106-
const { expanded, toggleExpanded } = useToolExpansion(initialExpanded);
106+
const { expanded, setExpanded, toggleExpanded } = useToolExpansion(initialExpanded);
107107
const [showRaw, setShowRaw] = React.useState(false);
108108

109+
// Update expanded state when result changes from undefined to WRITE DENIED
110+
// This handles the case where the component renders before the result is available
111+
React.useEffect(() => {
112+
if (result && !result.success && result.error?.startsWith(WRITE_DENIED_PREFIX)) {
113+
setExpanded(false);
114+
}
115+
}, [result, setExpanded]);
116+
109117
const filePath = "file_path" in args ? args.file_path : undefined;
110118

111119
// Copy to clipboard with feedback

src/components/tools/GenericToolCall.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ export const GenericToolCall: React.FC<GenericToolCallProps> = ({
3333
}) => {
3434
const { expanded, toggleExpanded } = useToolExpansion();
3535

36+
// Check if result contains an error
37+
// Handles two formats:
38+
// 1. Tool implementation errors: { success: false, error: "..." }
39+
// 2. AI SDK tool-error events: { error: "..." }
40+
const hasError =
41+
result &&
42+
typeof result === "object" &&
43+
"error" in result &&
44+
typeof result.error === "string" &&
45+
result.error.length > 0 &&
46+
(!("success" in result) || result.success === false);
47+
3648
const hasDetails = args !== undefined || result !== undefined;
3749

3850
return (
@@ -52,7 +64,16 @@ export const GenericToolCall: React.FC<GenericToolCallProps> = ({
5264
</DetailSection>
5365
)}
5466

55-
{result !== undefined && (
67+
{hasError ? (
68+
<DetailSection>
69+
<DetailLabel>Error</DetailLabel>
70+
<div className="text-danger bg-danger-overlay border-danger rounded border-l-2 px-2 py-1.5 text-[11px]">
71+
{String((result as { error: string }).error)}
72+
</div>
73+
</DetailSection>
74+
) : null}
75+
76+
{result !== undefined && !hasError && (
5677
<DetailSection>
5778
<DetailLabel>Result</DetailLabel>
5879
<DetailContent>{formatValue(result)}</DetailContent>

src/utils/messages/StreamingMessageAggregator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,9 +805,11 @@ export class StreamingMessageAggregator {
805805
});
806806
} else if (isDynamicToolPart(part)) {
807807
// Determine status based on part state and result
808+
// hasFailureResult now checks both error formats:
809+
// 1. Tool implementation errors: { success: false, error: "..." }
810+
// 2. AI SDK tool-error events: { error: "..." }
808811
let status: "pending" | "executing" | "completed" | "failed" | "interrupted";
809812
if (part.state === "output-available") {
810-
// Check if result indicates failure (for tools that return { success: boolean })
811813
status = hasFailureResult(part.output) ? "failed" : "completed";
812814
} else if (part.state === "input-available" && message.metadata?.partial) {
813815
status = "interrupted";

0 commit comments

Comments
 (0)