AssistantToolUseMessage.tsx
1 import { Box, Text } from 'ink' 2 import React from 'react' 3 import { logError } from '../../utils/log.js' 4 import { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 5 import { Tool } from '../../Tool.js' 6 import { Cost } from '../Cost.js' 7 import { ToolUseLoader } from '../ToolUseLoader.js' 8 import { getTheme } from '../../utils/theme.js' 9 import { BLACK_CIRCLE } from '../../constants/figures.js' 10 import { ThinkTool } from '../../tools/ThinkTool/ThinkTool.js' 11 import { AssistantThinkingMessage } from './AssistantThinkingMessage.js' 12 13 type Props = { 14 param: ToolUseBlockParam 15 costUSD: number 16 durationMs: number 17 addMargin: boolean 18 tools: Tool[] 19 debug: boolean 20 verbose: boolean 21 erroredToolUseIDs: Set<string> 22 inProgressToolUseIDs: Set<string> 23 unresolvedToolUseIDs: Set<string> 24 shouldAnimate: boolean 25 shouldShowDot: boolean 26 } 27 28 export function AssistantToolUseMessage({ 29 param, 30 costUSD, 31 durationMs, 32 addMargin, 33 tools, 34 debug, 35 verbose, 36 erroredToolUseIDs, 37 inProgressToolUseIDs, 38 unresolvedToolUseIDs, 39 shouldAnimate, 40 shouldShowDot, 41 }: Props): React.ReactNode { 42 const tool = tools.find(_ => _.name === param.name) 43 if (!tool) { 44 logError(`Tool ${param.name} not found`) 45 return null 46 } 47 const isQueued = 48 !inProgressToolUseIDs.has(param.id) && unresolvedToolUseIDs.has(param.id) 49 // Keeping color undefined makes the OS use the default color regardless of appearance 50 const color = isQueued ? getTheme().secondaryText : undefined 51 52 // TODO: Avoid this special case 53 if (tool === ThinkTool) { 54 // params were already validated in query(), so this won't throe 55 const { thought } = ThinkTool.inputSchema.parse(param.input) 56 return ( 57 <AssistantThinkingMessage 58 param={{ thinking: thought, signature: '', type: 'thinking' }} 59 addMargin={addMargin} 60 /> 61 ) 62 } 63 64 const userFacingToolName = tool.userFacingName(param.input as never) 65 return ( 66 <Box 67 flexDirection="row" 68 justifyContent="space-between" 69 marginTop={addMargin ? 1 : 0} 70 width="100%" 71 > 72 <Box> 73 <Box 74 flexWrap="nowrap" 75 minWidth={userFacingToolName.length + (shouldShowDot ? 2 : 0)} 76 > 77 {shouldShowDot && 78 (isQueued ? ( 79 <Box minWidth={2}> 80 <Text color={color}>{BLACK_CIRCLE}</Text> 81 </Box> 82 ) : ( 83 <ToolUseLoader 84 shouldAnimate={shouldAnimate} 85 isUnresolved={unresolvedToolUseIDs.has(param.id)} 86 isError={erroredToolUseIDs.has(param.id)} 87 /> 88 ))} 89 <Text color={color} bold={!isQueued}> 90 {userFacingToolName} 91 </Text> 92 </Box> 93 <Box flexWrap="nowrap"> 94 {Object.keys(param.input as { [key: string]: unknown }).length > 95 0 && ( 96 <Text color={color}> 97 ( 98 {tool.renderToolUseMessage(param.input as never, { 99 verbose, 100 })} 101 ) 102 </Text> 103 )} 104 <Text color={color}>…</Text> 105 </Box> 106 </Box> 107 <Cost costUSD={costUSD} durationMs={durationMs} debug={debug} /> 108 </Box> 109 ) 110 }