diff --git a/src/App.stories.tsx b/src/App.stories.tsx index 9cfcdce36..db7d01b8b 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -618,6 +618,88 @@ export const ActiveWorkspaceWithChat: Story = { }, }); + // User asking to test error handling + callback({ + id: "msg-10", + role: "user", + parts: [ + { + type: "text", + text: "Can you show me some examples of error handling?", + }, + ], + metadata: { + historySequence: 10, + timestamp: STABLE_TIMESTAMP - 160000, + }, + }); + + // Assistant response with various tool errors + callback({ + id: "msg-11", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll demonstrate various error scenarios:", + }, + // Tool error format #1: File read error + { + type: "dynamic-tool", + toolCallId: "call-5", + toolName: "file_read", + state: "output-available", + input: { + filePath: "/nonexistent/file.txt", + }, + output: { + success: false, + error: "ENOENT: no such file or directory, open '/nonexistent/file.txt'", + }, + }, + // Tool error format #2: WRITE DENIED (should be collapsed) + { + type: "dynamic-tool", + toolCallId: "call-6", + toolName: "file_edit_replace_string", + state: "output-available", + input: { + file_path: "src/test.ts", + old_string: "const x = 1;", + new_string: "const x = 2;", + }, + output: { + success: false, + error: + "WRITE DENIED, FILE UNMODIFIED: File has been modified since it was read. Please re-read the file and try again.", + }, + }, + // Tool error format #3: AI SDK error (policy disabled) + { + type: "dynamic-tool", + toolCallId: "call-7", + toolName: "unknown_tool", + state: "output-available", + input: { + someParam: "value", + }, + output: { + error: + "Tool 'unknown_tool' is not available. Available tools: bash, file_read, file_edit_replace_string", + }, + }, + { + type: "text", + text: "As you can see, different error types are displayed with clear indicators and appropriate styling.", + }, + ], + metadata: { + historySequence: 11, + timestamp: STABLE_TIMESTAMP - 150000, + model: "claude-sonnet-4-20250514", + }, + }); + // Mark as caught up callback({ type: "caught-up" }); }, 100); diff --git a/src/components/tools/FileEditToolCall.tsx b/src/components/tools/FileEditToolCall.tsx index cfb3d0034..e732beadc 100644 --- a/src/components/tools/FileEditToolCall.tsx +++ b/src/components/tools/FileEditToolCall.tsx @@ -103,9 +103,17 @@ 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); + // 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; // Copy to clipboard with feedback 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 1af90a9ac..1ee589721 100644 --- a/src/utils/messages/StreamingMessageAggregator.ts +++ b/src/utils/messages/StreamingMessageAggregator.ts @@ -805,9 +805,11 @@ export class StreamingMessageAggregator { }); } else if (isDynamicToolPart(part)) { // Determine status based on part state and result + // hasFailureResult now checks both error formats: + // 1. Tool implementation errors: { success: false, error: "..." } + // 2. AI SDK tool-error events: { error: "..." } let status: "pending" | "executing" | "completed" | "failed" | "interrupted"; if (part.state === "output-available") { - // Check if result indicates failure (for tools that return { success: boolean }) status = hasFailureResult(part.output) ? "failed" : "completed"; } else if (part.state === "input-available" && message.metadata?.partial) { status = "interrupted";