import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import React, { useContext } from 'react' import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js' import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js' import { BLACK_CIRCLE } from '../../constants/figures.js' import { Box, NoSelect, Text } from '@anthropic/ink' import { API_ERROR_MESSAGE_PREFIX, API_TIMEOUT_ERROR_MESSAGE, CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, CUSTOM_OFF_SWITCH_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH, PROMPT_TOO_LONG_ERROR_MESSAGE, startsWithApiErrorPrefix, TOKEN_REVOKED_ERROR_MESSAGE, } from '../../services/api/errors.js' import { isEmptyMessageText, NO_RESPONSE_REQUESTED, } from '../../utils/messages.js' import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js' import { getDefaultSonnetModel, renderModelName, } from '../../utils/model/model.js' import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js' import { CtrlOToExpand } from '../CtrlOToExpand.js' import { InterruptedByUser } from '../InterruptedByUser.js' import { Markdown } from '../Markdown.js' import { MessageResponse } from '../MessageResponse.js' import { MessageActionsSelectedContext } from '../messageActions.js' import { RateLimitMessage } from './RateLimitMessage.js' const MAX_API_ERROR_CHARS = 1000 type Props = { param: TextBlockParam addMargin: boolean shouldShowDot: boolean verbose: boolean width?: number | string onOpenRateLimitOptions?: () => void } function InvalidApiKeyMessage(): React.ReactNode { const isKeychainLocked = isMacOsKeychainLocked() return ( {INVALID_API_KEY_ERROR_MESSAGE} {isKeychainLocked && ( · Run in another terminal: security unlock-keychain )} ) } export function AssistantTextMessage({ param: { text }, addMargin, shouldShowDot, verbose, onOpenRateLimitOptions, }: Props): React.ReactNode { const isSelected = useContext(MessageActionsSelectedContext) if (isEmptyMessageText(text)) { return null } // Handle all rate limit error messages from getRateLimitErrorMessage // Use the exported function to avoid fragile string coupling if (isRateLimitErrorMessage(text)) { return ( ) } switch (text) { // Local JSX commands don't need a response, but we still want Claude to see them // Tool results render their own interrupt messages case NO_RESPONSE_REQUESTED: return null case PROMPT_TOO_LONG_ERROR_MESSAGE: { const upgradeHint = getUpgradeMessage('warning') return ( Context limit reached · /compact or /clear to continue {upgradeHint ? ` · ${upgradeHint}` : ''} ) } case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: return ( Credit balance too low · Add funds: https://platform.claude.com/settings/billing ) case INVALID_API_KEY_ERROR_MESSAGE: return case INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: return ( {INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL} ) case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY: case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH: return ( {text} ) case TOKEN_REVOKED_ERROR_MESSAGE: return ( {TOKEN_REVOKED_ERROR_MESSAGE} ) case API_TIMEOUT_ERROR_MESSAGE: return ( {API_TIMEOUT_ERROR_MESSAGE} {process.env.API_TIMEOUT_MS && ( <> {' '} (API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing it) )} ) case CUSTOM_OFF_SWITCH_MESSAGE: return ( We are experiencing high demand for Opus 4. To continue immediately, use /model to switch to{' '} {renderModelName(getDefaultSonnetModel())} and continue coding. ) // TODO: Move this to a user turn case ERROR_MESSAGE_USER_ABORT: return ( ) default: if (startsWithApiErrorPrefix(text)) { const truncated = !verbose && text.length > MAX_API_ERROR_CHARS return ( {text === API_ERROR_MESSAGE_PREFIX ? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.` : truncated ? text.slice(0, MAX_API_ERROR_CHARS) + '…' : text} {truncated && } ) } return ( {shouldShowDot && ( {BLACK_CIRCLE} )} {text} ) } }