// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { Box, Text } from '@anthropic/ink'; import * as React from 'react'; import { getLargeMemoryFiles, MAX_MEMORY_CHARACTER_COUNT, type MemoryFileInfo } from './claudemd.js'; import figures from 'figures'; import { getCwd } from './cwd.js'; import { relative } from 'path'; import { formatNumber } from './format.js'; import type { getGlobalConfig } from './config.js'; import { getAnthropicApiKeyWithSource, getApiKeyFromConfigOrMacOSKeychain, getAuthTokenSource, isClaudeAISubscriber, } from './auth.js'; import type { AgentDefinitionsResult } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'; import { getAgentDescriptionsTotalTokens, AGENT_DESCRIPTIONS_THRESHOLD } from './statusNoticeHelpers.js'; import { isSupportedJetBrainsTerminal, toIDEDisplayName, getTerminalIdeType } from './ide.js'; import { isJetBrainsPluginInstalledCachedSync } from './jetbrains.js'; // Types export type StatusNoticeType = 'warning' | 'info'; export type StatusNoticeContext = { config: ReturnType; agentDefinitions?: AgentDefinitionsResult; memoryFiles: MemoryFileInfo[]; }; export type StatusNoticeDefinition = { id: string; type: StatusNoticeType; isActive: (context: StatusNoticeContext) => boolean; render: (context: StatusNoticeContext) => React.ReactNode; }; // Individual notice definitions const largeMemoryFilesNotice: StatusNoticeDefinition = { id: 'large-memory-files', type: 'warning', isActive: ctx => getLargeMemoryFiles(ctx.memoryFiles).length > 0, render: ctx => { const largeMemoryFiles = getLargeMemoryFiles(ctx.memoryFiles); return ( <> {largeMemoryFiles.map(file => { const displayPath = file.path.startsWith(getCwd()) ? relative(getCwd(), file.path) : file.path; return ( {figures.warning} Large {displayPath} will impact performance ({formatNumber(file.content.length)} chars > {formatNumber(MAX_MEMORY_CHARACTER_COUNT)}) · /memory to edit ); })} ); }, }; const claudeAiSubscriberExternalTokenNotice: StatusNoticeDefinition = { id: 'claude-ai-external-token', type: 'warning', isActive: () => { const authTokenInfo = getAuthTokenSource(); return ( isClaudeAISubscriber() && (authTokenInfo.source === 'ANTHROPIC_AUTH_TOKEN' || authTokenInfo.source === 'apiKeyHelper') ); }, render: () => { const authTokenInfo = getAuthTokenSource(); return ( {figures.warning} Auth conflict: Using {authTokenInfo.source} instead of Claude account subscription token. Either unset{' '} {authTokenInfo.source}, or run `claude /logout`. ); }, }; const apiKeyConflictNotice: StatusNoticeDefinition = { id: 'api-key-conflict', type: 'warning', isActive: () => { const { source: apiKeySource } = getAnthropicApiKeyWithSource({ skipRetrievingKeyFromApiKeyHelper: true, }); return ( !!getApiKeyFromConfigOrMacOSKeychain() && (apiKeySource === 'ANTHROPIC_API_KEY' || apiKeySource === 'apiKeyHelper') ); }, render: () => { const { source: apiKeySource } = getAnthropicApiKeyWithSource({ skipRetrievingKeyFromApiKeyHelper: true, }); return ( {figures.warning} Auth conflict: Using {apiKeySource} instead of Anthropic Console key. Either unset {apiKeySource}, or run `claude /logout`. ); }, }; const bothAuthMethodsNotice: StatusNoticeDefinition = { id: 'both-auth-methods', type: 'warning', isActive: () => { const { source: apiKeySource } = getAnthropicApiKeyWithSource({ skipRetrievingKeyFromApiKeyHelper: true, }); const authTokenInfo = getAuthTokenSource(); return ( apiKeySource !== 'none' && authTokenInfo.source !== 'none' && !(apiKeySource === 'apiKeyHelper' && authTokenInfo.source === 'apiKeyHelper') ); }, render: () => { const { source: apiKeySource } = getAnthropicApiKeyWithSource({ skipRetrievingKeyFromApiKeyHelper: true, }); const authTokenInfo = getAuthTokenSource(); return ( {figures.warning} Auth conflict: Both a token ({authTokenInfo.source}) and an API key ({apiKeySource}) are set. This may lead to unexpected behavior. · Trying to use {authTokenInfo.source === 'claude.ai' ? 'claude.ai' : authTokenInfo.source}?{' '} {apiKeySource === 'ANTHROPIC_API_KEY' ? 'Unset the ANTHROPIC_API_KEY environment variable, or claude /logout then say "No" to the API key approval before login.' : apiKeySource === 'apiKeyHelper' ? 'Unset the apiKeyHelper setting.' : 'claude /logout'} · Trying to use {apiKeySource}?{' '} {authTokenInfo.source === 'claude.ai' ? 'claude /logout to sign out of claude.ai.' : `Unset the ${authTokenInfo.source} environment variable.`} ); }, }; const largeAgentDescriptionsNotice: StatusNoticeDefinition = { id: 'large-agent-descriptions', type: 'warning', isActive: context => { const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions); return totalTokens > AGENT_DESCRIPTIONS_THRESHOLD; }, render: context => { const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions); return ( {figures.warning} Large cumulative agent descriptions will impact performance (~ {formatNumber(totalTokens)} tokens > {formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)}) · /agents to manage ); }, }; const jetbrainsPluginNotice: StatusNoticeDefinition = { id: 'jetbrains-plugin-install', type: 'info', isActive: context => { // Only show if running in JetBrains built-in terminal if (!isSupportedJetBrainsTerminal()) { return false; } // Don't show if auto-install is disabled const shouldAutoInstall = context.config.autoInstallIdeExtension ?? true; if (!shouldAutoInstall) { return false; } // Check if plugin is already installed (cached to avoid repeated filesystem checks) const ideType = getTerminalIdeType(); return ideType !== null && !isJetBrainsPluginInstalledCachedSync(ideType); }, render: () => { const ideType = getTerminalIdeType(); const ideName = toIDEDisplayName(ideType); return ( {figures.arrowUp} Install the {ideName} plugin from the JetBrains Marketplace:{' '} https://docs.claude.com/s/claude-code-jetbrains ); }, }; // All notice definitions export const statusNoticeDefinitions: StatusNoticeDefinition[] = [ largeMemoryFilesNotice, largeAgentDescriptionsNotice, claudeAiSubscriberExternalTokenNotice, apiKeyConflictNotice, bothAuthMethodsNotice, jetbrainsPluginNotice, ]; // Helper functions for external use export function getActiveNotices(context: StatusNoticeContext): StatusNoticeDefinition[] { return statusNoticeDefinitions.filter(notice => notice.isActive(context)); }