import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import React, { useMemo } from 'react' import { useTerminalSize } from 'src/hooks/useTerminalSize.js' import type { ThemeName } from 'src/utils/theme.js' import type { Command } from '../../commands.js' import { BLACK_CIRCLE } from '../../constants/figures.js' import { Box, Text, stringWidth, useTheme } from '@anthropic/ink' import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js' import { findToolByName, type Tool, type ToolProgressData, type Tools, } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js' import { logError } from '../../utils/log.js' import type { buildMessageLookups } from '../../utils/messages.js' import { MessageResponse } from '../MessageResponse.js' import { useSelectedMessageBg } from '../messageActions.js' import { SentryErrorBoundary } from '../SentryErrorBoundary.js' import { ToolUseLoader } from '../ToolUseLoader.js' import { HookProgressMessage } from './HookProgressMessage.js' type Props = { param: ToolUseBlockParam addMargin: boolean tools: Tools commands: Command[] verbose: boolean inProgressToolUseIDs: Set progressMessagesForMessage: ProgressMessage[] shouldAnimate: boolean shouldShowDot: boolean inProgressToolCallCount?: number lookups: ReturnType isTranscriptMode?: boolean } export function AssistantToolUseMessage({ param, addMargin, tools, commands, verbose, inProgressToolUseIDs, progressMessagesForMessage, shouldAnimate, shouldShowDot, inProgressToolCallCount, lookups, isTranscriptMode, }: Props): React.ReactNode { const terminalSize = useTerminalSize() const [theme] = useTheme() const bg = useSelectedMessageBg() const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider( state => state.pendingWorkerRequest, ) const isClassifierCheckingRaw = useIsClassifierChecking(param.id) const permissionMode = useAppStateMaybeOutsideOfProvider( state => state.toolPermissionContext.mode, ) // strippedDangerousRules is set by stripDangerousPermissionsForAutoMode // (even to {}) whenever auto is active, and cleared by restoreDangerousPermissions // on deactivation — a reliable proxy for isAutoModeActive() during plan. // prePlanMode would be stale after transitionPlanAutoMode deactivates mid-plan. const hasStrippedRules = useAppStateMaybeOutsideOfProvider( state => !!state.toolPermissionContext.strippedDangerousRules, ) const isAutoClassifier = permissionMode === 'auto' || (permissionMode === 'plan' && hasStrippedRules) const isClassifierChecking = process.env.USER_TYPE === 'ant' && isClassifierCheckingRaw && permissionMode !== 'auto' // Memoize on param identity (stable — from the persisted message object). // Zod safeParse allocates per call, and some tools' userFacingName() // (BashTool → shouldUseSandbox → shell-quote parse) are expensive. Without // this, ~50 bash messages × shell-quote-per-render pushed transition // render past the shimmer tick → abort → infinite retry (#21605). const parsed = useMemo(() => { if (!tools) return null const tool = findToolByName(tools, param.name) if (!tool) return null const input = tool.inputSchema.safeParse(param.input) const data = input.success ? input.data : undefined return { tool, input, userFacingToolName: tool.userFacingName(data), userFacingToolNameBackgroundColor: tool.userFacingNameBackgroundColor?.(data), isTransparentWrapper: tool.isTransparentWrapper?.() ?? false, } }, [tools, param]) if (!parsed) { // Guard against undefined tools (required prop) or unknown tool name logError( new Error( tools ? `Tool ${param.name} not found` : `Tools array is undefined for tool ${param.name}`, ), ) return null } const { tool, input, userFacingToolName, userFacingToolNameBackgroundColor, isTransparentWrapper, } = parsed const isResolved = lookups.resolvedToolUseIDs.has(param.id) const isQueued = !inProgressToolUseIDs.has(param.id) && !isResolved const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id if (isTransparentWrapper) { if (isQueued || isResolved) return null return ( {renderToolUseProgressMessage( tool, tools, lookups, param.id, progressMessagesForMessage, { verbose, inProgressToolCallCount, isTranscriptMode }, terminalSize, )} ) } if (userFacingToolName === '') { return null } const renderedToolUseMessage = input.success ? renderToolUseMessage(tool, input.data, { theme, verbose, commands }) : null if (renderedToolUseMessage === null) { return null } return ( {shouldShowDot && (isQueued ? ( {BLACK_CIRCLE} ) : ( // WARNING: The code here and in ToolUseLoader is particularly // sensitive to what *should* just be trivial refactorings. See // the comment in ToolUseLoader for more details. ))} {userFacingToolName} {renderedToolUseMessage !== '' && ( ({renderedToolUseMessage}) )} {/* Render tool-specific tags (timeout, model, resume ID, etc.) */} {input.success && tool.renderToolUseTag && tool.renderToolUseTag(input.data)} {!isResolved && !isQueued && (isClassifierChecking ? ( {isAutoClassifier ? 'Auto classifier checking\u2026' : 'Bash classifier checking\u2026'} ) : isWaitingForPermission ? ( Waiting for permission… ) : ( renderToolUseProgressMessage( tool, tools, lookups, param.id, progressMessagesForMessage, { verbose, inProgressToolCallCount, isTranscriptMode, }, terminalSize, ) ))} {!isResolved && isQueued && renderToolUseQueuedMessage(tool)} ) } function renderToolUseMessage( tool: Tool, input: unknown, { theme, verbose, commands, }: { theme: ThemeName; verbose: boolean; commands: Command[] }, ): React.ReactNode { try { const parsed = tool.inputSchema.safeParse(input) if (!parsed.success) { return '' } return tool.renderToolUseMessage(parsed.data, { theme, verbose, commands }) } catch (error) { logError( new Error(`Error rendering tool use message for ${tool.name}: ${error}`), ) return '' } } function renderToolUseProgressMessage( tool: Tool, tools: Tools, lookups: ReturnType, toolUseID: string, progressMessagesForMessage: ProgressMessage[], { verbose, inProgressToolCallCount, isTranscriptMode, }: { verbose: boolean inProgressToolCallCount?: number isTranscriptMode?: boolean }, terminalSize: { columns: number; rows: number }, ): React.ReactNode { const toolProgressMessages = progressMessagesForMessage.filter( (msg): msg is ProgressMessage => (msg.data as Record).type !== 'hook_progress', ) try { const toolMessages = tool.renderToolUseProgressMessage?.(toolProgressMessages, { tools, verbose, terminalSize, inProgressToolCallCount: inProgressToolCallCount ?? 1, isTranscriptMode, }) ?? null return ( <> {toolMessages} ) } catch (error) { logError( new Error( `Error rendering tool use progress message for ${tool.name}: ${error}`, ), ) return null } } function renderToolUseQueuedMessage(tool: Tool): React.ReactNode { try { return tool.renderToolUseQueuedMessage?.() } catch (error) { logError( new Error( `Error rendering tool use queued message for ${tool.name}: ${error}`, ), ) return null } }