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}
)
}
}