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; defaultCollapsed?: boolean; }; export function AssistantToolUseMessage({ param, addMargin, tools, commands, verbose, inProgressToolUseIDs, progressMessagesForMessage, shouldAnimate, shouldShowDot, inProgressToolCallCount, lookups, isTranscriptMode, defaultCollapsed, }: 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 && !defaultCollapsed && (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; } }