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')} -
- )} -
- )} ) : (