AssistantTextMessage.tsx
1 import { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' 2 import React from 'react' 3 import { AssistantBashOutputMessage } from './AssistantBashOutputMessage.js' 4 import { AssistantLocalCommandOutputMessage } from './AssistantLocalCommandOutputMessage.js' 5 import { getTheme } from '../../utils/theme.js' 6 import { Box, Text } from 'ink' 7 import { Cost } from '../Cost.js' 8 import { 9 API_ERROR_MESSAGE_PREFIX, 10 CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, 11 INVALID_API_KEY_ERROR_MESSAGE, 12 PROMPT_TOO_LONG_ERROR_MESSAGE, 13 } from '../../services/claude.js' 14 import { 15 CANCEL_MESSAGE, 16 INTERRUPT_MESSAGE, 17 INTERRUPT_MESSAGE_FOR_TOOL_USE, 18 isEmptyMessageText, 19 NO_RESPONSE_REQUESTED, 20 } from '../../utils/messages.js' 21 import { BLACK_CIRCLE } from '../../constants/figures.js' 22 import { applyMarkdown } from '../../utils/markdown.js' 23 import { useTerminalSize } from '../../hooks/useTerminalSize.js' 24 25 type Props = { 26 param: TextBlockParam 27 costUSD: number 28 durationMs: number 29 debug: boolean 30 addMargin: boolean 31 shouldShowDot: boolean 32 verbose?: boolean 33 width?: number | string 34 } 35 36 export function AssistantTextMessage({ 37 param: { text }, 38 costUSD, 39 durationMs, 40 debug, 41 addMargin, 42 shouldShowDot, 43 verbose, 44 }: Props): React.ReactNode { 45 const { columns } = useTerminalSize() 46 if (isEmptyMessageText(text)) { 47 return null 48 } 49 50 // Show bash output 51 if (text.startsWith('<bash-stdout') || text.startsWith('<bash-stderr')) { 52 return <AssistantBashOutputMessage content={text} verbose={verbose} /> 53 } 54 55 // Show command output 56 if ( 57 text.startsWith('<local-command-stdout') || 58 text.startsWith('<local-command-stderr') 59 ) { 60 return <AssistantLocalCommandOutputMessage content={text} /> 61 } 62 63 if (text.startsWith(API_ERROR_MESSAGE_PREFIX)) { 64 return ( 65 <Text> 66 ⎿ 67 <Text color={getTheme().error}> 68 {text === API_ERROR_MESSAGE_PREFIX 69 ? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.` 70 : text} 71 </Text> 72 </Text> 73 ) 74 } 75 76 switch (text) { 77 // Local JSX commands don't need a response, but we still want Claude to see them 78 // Tool results render their own interrupt messages 79 case NO_RESPONSE_REQUESTED: 80 case INTERRUPT_MESSAGE_FOR_TOOL_USE: 81 return null 82 83 case INTERRUPT_MESSAGE: 84 case CANCEL_MESSAGE: 85 return ( 86 <Text> 87 ⎿ 88 <Text color={getTheme().error}>Interrupted by user</Text> 89 </Text> 90 ) 91 92 case PROMPT_TOO_LONG_ERROR_MESSAGE: 93 return ( 94 <Text> 95 ⎿ 96 <Text color={getTheme().error}> 97 Context low · Run /compact to compact & continue 98 </Text> 99 </Text> 100 ) 101 102 case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: 103 return ( 104 <Text> 105 ⎿ 106 <Text color={getTheme().error}> 107 Credit balance too low · Add funds: 108 https://console.anthropic.com/settings/billing 109 </Text> 110 </Text> 111 ) 112 113 case INVALID_API_KEY_ERROR_MESSAGE: 114 return ( 115 <Text> 116 ⎿ 117 <Text color={getTheme().error}>{INVALID_API_KEY_ERROR_MESSAGE}</Text> 118 </Text> 119 ) 120 121 default: 122 return ( 123 <Box 124 alignItems="flex-start" 125 flexDirection="row" 126 justifyContent="space-between" 127 marginTop={addMargin ? 1 : 0} 128 width="100%" 129 > 130 <Box flexDirection="row"> 131 {shouldShowDot && ( 132 <Box minWidth={2}> 133 <Text color={getTheme().text}>{BLACK_CIRCLE}</Text> 134 </Box> 135 )} 136 <Box flexDirection="column" width={columns - 6}> 137 <Text>{applyMarkdown(text)}</Text> 138 </Box> 139 </Box> 140 <Cost costUSD={costUSD} durationMs={durationMs} debug={debug} /> 141 </Box> 142 ) 143 } 144 }