From bb8db5815c2d20ee4fbfa02d14c886a56ef352e0 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:16:34 +0300 Subject: [PATCH] fix: show Claude tool result errors Claude stores some tool failures as errored tool_result rows. The UI either attached those rows to hidden tool output or dropped them when no matching tool call was rendered, which made validation failures disappear from chat history. Render unattached errored tool results, unwrap Claude tool_use_error content, and keep tool errors visible even for tools whose successful output is hidden. Also remove the permission-grant recovery controls from rendered error history so denied tool use stays a plain error message. --- src/components/chat/hooks/useChatMessages.ts | 32 +++++++++- .../chat/tools/configs/toolConfigs.ts | 6 +- .../view/subcomponents/MessageComponent.tsx | 61 +------------------ 3 files changed, 38 insertions(+), 61 deletions(-) diff --git a/src/components/chat/hooks/useChatMessages.ts b/src/components/chat/hooks/useChatMessages.ts index 1590c4af..82e7d9e1 100644 --- a/src/components/chat/hooks/useChatMessages.ts +++ b/src/components/chat/hooks/useChatMessages.ts @@ -7,6 +7,12 @@ import type { NormalizedMessage } from '../../../stores/useSessionStore'; import type { ChatMessage, SubagentChildTool } from '../types/types'; import { decodeHtmlEntities, unescapeWithMathProtection, formatUsageLimitText } from '../utils/chatFormatting'; +function formatToolResultContent(content: unknown): string { + const text = typeof content === 'string' ? content : JSON.stringify(content); + const toolUseErrorMatch = /^([\s\S]*)<\/tool_use_error>$/.exec(text.trim()); + return toolUseErrorMatch ? toolUseErrorMatch[1] : text; +} + /** * Convert NormalizedMessage[] from the session store into ChatMessage[] * that the existing UI components expect. @@ -20,7 +26,12 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes // First pass: collect tool results for attachment const toolResultMap = new Map(); + const toolUseIds = new Set(); for (const msg of messages) { + if (msg.kind === 'tool_use' && msg.toolId) { + toolUseIds.add(msg.toolId); + } + if (msg.kind === 'tool_result' && msg.toolId) { toolResultMap.set(msg.toolId, msg); } @@ -97,7 +108,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes const toolResult = tr ? { - content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content), + content: formatToolResultContent(tr.content), isError: Boolean(tr.isError), toolUseResult: (tr as any).toolUseResult, } @@ -191,8 +202,25 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes break; // tool_result is handled via attachment to tool_use above - case 'tool_result': + case 'tool_result': { + if (msg.toolId && toolUseIds.has(msg.toolId)) { + break; + } + + const content = formatToolResultContent(msg.content || ''); + if (!content.trim()) { + break; + } + + converted.push({ + type: msg.isError ? 'error' : 'assistant', + content, + timestamp: msg.timestamp, + toolId: msg.toolId, + ...sharedMetadata, + }); break; + } default: break; diff --git a/src/components/chat/tools/configs/toolConfigs.ts b/src/components/chat/tools/configs/toolConfigs.ts index fbb3f2d4..6a34b2cd 100644 --- a/src/components/chat/tools/configs/toolConfigs.ts +++ b/src/components/chat/tools/configs/toolConfigs.ts @@ -564,11 +564,15 @@ export function shouldHideToolResult(toolName: string, toolResult: any): boolean if (!config.result) return false; + // Hidden/success-only configs suppress noisy successful output, but errors + // still need to be visible so failed tool calls are diagnosable. + if (toolResult?.isError) return false; + // Always hidden if (config.result.hidden) return true; // Hide on success only - if (config.result.hideOnSuccess && toolResult && !toolResult.isError) { + if (config.result.hideOnSuccess && toolResult) { return true; } diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx index c40e0b94..ba69bf90 100644 --- a/src/components/chat/view/subcomponents/MessageComponent.tsx +++ b/src/components/chat/view/subcomponents/MessageComponent.tsx @@ -1,5 +1,6 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; + import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; import type { ChatMessage, @@ -8,10 +9,10 @@ import type { Provider, } from '../../types/types'; import { formatUsageLimitText } from '../../utils/chatFormatting'; -import { getClaudePermissionSuggestion } from '../../utils/chatPermissions'; import type { Project } from '../../../../types/app'; import { ToolRenderer, shouldHideToolResult } from '../../tools'; import { Reasoning, ReasoningTrigger, ReasoningContent } from '../../../../shared/view/ui'; + import { Markdown } from './Markdown'; import MessageCopyControl from './MessageCopyControl'; @@ -41,10 +42,9 @@ type InteractiveOption = { isSelected: boolean; }; -type PermissionGrantState = 'idle' | 'granted' | 'error'; const COPY_HIDDEN_TOOL_NAMES = new Set(['Bash', 'Edit', 'Write', 'ApplyPatch']); -const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => { +const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => { const { t } = useTranslation('chat'); const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || @@ -53,8 +53,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o (prevMessage.type === 'error')); const messageRef = useRef(null); const [isExpanded, setIsExpanded] = useState(false); - const permissionSuggestion = getClaudePermissionSuggestion(message, provider); - const [permissionGrantState, setPermissionGrantState] = useState('idle'); const userCopyContent = String(message.content || ''); const formattedMessageContent = useMemo( () => formatUsageLimitText(String(message.content || '')), @@ -73,10 +71,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o !message.isThinking; - useEffect(() => { - setPermissionGrantState('idle'); - }, [permissionSuggestion?.entry, message.toolId]); - useEffect(() => { const node = messageRef.current; if (!autoExpandTools || !node || !message.isToolUse) return; @@ -241,55 +235,6 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o {String(message.toolResult.content || '')} - {permissionSuggestion && ( -
-
- - {onShowSettings && ( - - )} -
-
- {t('permissions.addTo', { entry: permissionSuggestion.entry })} -
- {permissionGrantState === 'error' && ( -
- {t('permissions.error')} -
- )} - {(permissionSuggestion.isAllowed || permissionGrantState === 'granted') && ( -
- {t('permissions.retry')} -
- )} -
- )} ) : (