/ src / components / messages / AssistantTextMessage.tsx
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          &nbsp;&nbsp;⎿ &nbsp;
 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            &nbsp;&nbsp;⎿ &nbsp;
 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            &nbsp;&nbsp;⎿ &nbsp;
 96            <Text color={getTheme().error}>
 97              Context low &middot; Run /compact to compact & continue
 98            </Text>
 99          </Text>
100        )
101  
102      case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE:
103        return (
104          <Text>
105            &nbsp;&nbsp;⎿ &nbsp;
106            <Text color={getTheme().error}>
107              Credit balance too low &middot; 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            &nbsp;&nbsp;⎿ &nbsp;
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  }