/ src / components / messages / AssistantToolUseMessage.tsx
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  }