import { feature } from 'bun:bundle' import figures from 'figures' import * as React from 'react' import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js' import { Box, Text, useTheme } from '@anthropic/ink' import { useAppState } from '../../../state/AppState.js' import { filterToolProgressMessages, type Tool, type Tools, } from '../../../Tool.js' import type { NormalizedUserMessage, ProgressMessage, } from '../../../types/message.js' import { deleteClassifierApproval, getClassifierApproval, getYoloClassifierApproval, } from '../../../utils/classifierApprovals.js' import type { buildMessageLookups } from '../../../utils/messages.js' import { MessageResponse } from '../../MessageResponse.js' import { HookProgressMessage } from '../HookProgressMessage.js' type Props = { message: NormalizedUserMessage lookups: ReturnType toolUseID: string progressMessagesForMessage: ProgressMessage[] style?: 'condensed' tool?: Tool tools: Tools verbose: boolean width: number | string isTranscriptMode?: boolean } export function UserToolSuccessMessage({ message, lookups, toolUseID, progressMessagesForMessage, style, tool, tools, verbose, width, isTranscriptMode, }: Props): React.ReactNode { const [theme] = useTheme() // Hook stays inside feature() ternary so external builds don't pay a // per-scrollback-message store subscription — same pattern as // UserPromptMessage.tsx. const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ? useAppState(s => s.isBriefOnly) : false // Capture classifier approval once on mount, then delete from Map to prevent linear growth. // useState lazy initializer ensures the value persists across re-renders. const [classifierRule] = React.useState(() => getClassifierApproval(toolUseID), ) const [yoloReason] = React.useState(() => getYoloClassifierApproval(toolUseID), ) React.useEffect(() => { deleteClassifierApproval(toolUseID) }, [toolUseID]) if (!message.toolUseResult || !tool) { return null } // Resumed transcripts deserialize toolUseResult via raw JSON.parse with no // validation (parseJSONL). A partial/corrupt/old-format result crashes // renderToolResultMessage on first field access (anthropics/claude-code#39817). // Validate against outputSchema before rendering — mirrors CollapsedReadSearchContent. const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult) if (parsedOutput && !parsedOutput.success) { return null } const toolResult = parsedOutput?.data ?? message.toolUseResult const renderedMessage = tool.renderToolResultMessage?.( toolResult as never, filterToolProgressMessages(progressMessagesForMessage), { style, theme, tools, verbose, isTranscriptMode, isBriefOnly, input: lookups.toolUseByToolUseID.get(toolUseID)?.input, }, ) ?? null // Don't render anything if the tool result message is null if (renderedMessage === null) { return null } // Tools that return '' from userFacingName opt out of tool chrome and // render like plain assistant text. Skip the tool-result width constraint // so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col // dot gutter) holds — otherwise tables wrap their box-drawing chars. const rendersAsAssistantText = tool.userFacingName(undefined) === '' return ( {renderedMessage} {feature('BASH_CLASSIFIER') ? classifierRule && ( {figures.tick} {' Auto-approved \u00b7 matched '} {`"${classifierRule}"`} ) : null} {feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && ( Allowed by auto mode classifier ) : null} ) }