claude-code/utils/statusNoticeDefinitions.tsx

198 lines
30 KiB
TypeScript
Raw Normal View History

// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { Box, Text } from '../ink.js';
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 '../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<typeof getGlobalConfig>;
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 <Box key={file.path} flexDirection="row">
<Text color="warning">{figures.warning}</Text>
<Text color="warning">
Large <Text bold>{displayPath}</Text> will impact performance (
{formatNumber(file.content.length)} chars &gt;{' '}
{formatNumber(MAX_MEMORY_CHARACTER_COUNT)})
<Text dimColor> · /memory to edit</Text>
</Text>
</Box>;
})}
</>;
}
};
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 <Box flexDirection="row" marginTop={1}>
<Text color="warning">{figures.warning}</Text>
<Text color="warning">
Auth conflict: Using {authTokenInfo.source} instead of Claude account
subscription token. Either unset {authTokenInfo.source}, or run
`claude /logout`.
</Text>
</Box>;
}
};
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 <Box flexDirection="row" marginTop={1}>
<Text color="warning">{figures.warning}</Text>
<Text color="warning">
Auth conflict: Using {apiKeySource} instead of Anthropic Console key.
Either unset {apiKeySource}, or run `claude /logout`.
</Text>
</Box>;
}
};
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 <Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
<Text color="warning">
Auth conflict: Both a token ({authTokenInfo.source}) and an API key
({apiKeySource}) are set. This may lead to unexpected behavior.
</Text>
</Box>
<Box flexDirection="column" marginLeft={3}>
<Text color="warning">
· 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'}
</Text>
<Text color="warning">
· Trying to use {apiKeySource}?{' '}
{authTokenInfo.source === 'claude.ai' ? 'claude /logout to sign out of claude.ai.' : `Unset the ${authTokenInfo.source} environment variable.`}
</Text>
</Box>
</Box>;
}
};
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 <Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
<Text color="warning">
Large cumulative agent descriptions will impact performance (~
{formatNumber(totalTokens)} tokens &gt;{' '}
{formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)})
<Text dimColor> · /agents to manage</Text>
</Text>
</Box>;
}
};
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 <Box flexDirection="row" gap={1} marginLeft={1}>
<Text color="ide">{figures.arrowUp}</Text>
<Text>
Install the <Text color="ide">{ideName}</Text> plugin from the
JetBrains Marketplace:{' '}
<Text bold>https://docs.claude.com/s/claude-code-jetbrains</Text>
</Text>
</Box>;
}
};
// 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));
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJCb3giLCJUZXh0IiwiUmVhY3QiLCJnZXRMYXJnZU1lbW9yeUZpbGVzIiwiTUFYX01FTU9SWV9DSEFSQUNURVJfQ09VTlQiLCJNZW1vcnlGaWxlSW5mbyIsImZpZ3VyZXMiLCJnZXRDd2QiLCJyZWxhdGl2ZSIsImZvcm1hdE51bWJlciIsImdldEdsb2JhbENvbmZpZyIsImdldEFudGhyb3BpY0FwaUtleVdpdGhTb3VyY2UiLCJnZXRBcGlLZXlGcm9tQ29uZmlnT3JNYWNPU0tleWNoYWluIiwiZ2V0QXV0aFRva2VuU291cmNlIiwiaXNDbGF1ZGVBSVN1YnNjcmliZXIiLCJBZ2VudERlZmluaXRpb25zUmVzdWx0IiwiZ2V0QWdlbnREZXNjcmlwdGlvbnNUb3RhbFRva2VucyIsIkFHRU5UX0RFU0NSSVBUSU9OU19USFJFU0hPTEQiLCJpc1N1cHBvcnRlZEpldEJyYWluc1Rlcm1pbmFsIiwidG9JREVEaXNwbGF5TmFtZSIsImdldFRlcm1pbmFsSWRlVHlwZSIsImlzSmV0QnJhaW5zUGx1Z2luSW5zdGFsbGVkQ2FjaGVkU3luYyIsIlN0YXR1c05vdGljZVR5cGUiLCJTdGF0dXNOb3RpY2VDb250ZXh0IiwiY29uZmlnIiwiUmV0dXJuVHlwZSIsImFnZW50RGVmaW5pdGlvbnMiLCJtZW1vcnlGaWxlcyIsIlN0YXR1c05vdGljZURlZmluaXRpb24iLCJpZCIsInR5cGUiLCJpc0FjdGl2ZSIsImNvbnRleHQiLCJyZW5kZXIiLCJSZWFjdE5vZGUiLCJsYXJnZU1lbW9yeUZpbGVzTm90aWNlIiwiY3R4IiwibGVuZ3RoIiwibGFyZ2VNZW1vcnlGaWxlcyIsIm1hcCIsImZpbGUiLCJkaXNwbGF5UGF0aCIsInBhdGgiLCJzdGFydHNXaXRoIiwid2FybmluZyIsImNvbnRlbnQiLCJjbGF1ZGVBaVN1YnNjcmliZXJFeHRlcm5hbFRva2VuTm90aWNlIiwiYXV0aFRva2VuSW5mbyIsInNvdXJjZSIsImFwaUtleUNvbmZsaWN0Tm90aWNlIiwiYXBpS2V5U291cmNlIiwic2tpcFJldHJpZXZpbmdLZXlGcm9tQXBpS2V5SGVscGVyIiwiYm90aEF1dGhNZXRob2RzTm90aWNlIiwibGFyZ2VBZ2VudERlc2NyaXB0aW9uc05vdGljZSIsInRvdGFsVG9rZW5zIiwiamV0YnJhaW5zUGx1Z2luTm90aWNlIiwic2hvdWxkQXV0b0luc3RhbGwiLCJhdXRvSW5zdGFsbElkZUV4dGVuc2lvbiIsImlkZVR5cGUiLCJpZGVOYW1lIiwiYXJyb3dVcCIsInN0YXR1c05vdGljZURlZmluaXRpb25zIiwiZ2V0QWN0aXZlTm90aWNlcyIsImZpbHRlciIsIm5vdGljZSJdLCJzb3VyY2VzIjpbInN0YXR1c05vdGljZURlZmluaXRpb25zLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBiaW9tZS1pZ25vcmUtYWxsIGFzc2lzdC9zb3VyY2Uvb3JnYW5pemVJbXBvcnRzOiBBTlQtT05MWSBpbXBvcnQgbWFya2VycyBtdXN0IG5vdCBiZSByZW9yZGVyZWRcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgZ2V0TGFyZ2VNZW1vcnlGaWxlcyxcbiAgTUFYX01FTU9SWV9DSEFSQUNURVJfQ09VTlQsXG4gIHR5cGUgTWVtb3J5RmlsZUluZm8sXG59IGZyb20gJy4vY2xhdWRlbWQuanMnXG5pbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IHsgZ2V0Q3dkIH0gZnJvbSAnLi9jd2QuanMnXG5pbXBvcnQgeyByZWxhdGl2ZSB9IGZyb20gJ3BhdGgnXG5pbXBvcnQgeyBmb3JtYXROdW1iZXIgfSBmcm9tICcuL2Zvcm1hdC5qcydcbmltcG9ydCB0eXBlIHsgZ2V0R2xvYmFsQ29uZmlnIH0gZnJvbSAnLi9jb25maWcuanMnXG5pbXBvcnQge1xuICBnZXRBbnRocm9waWNBcGlLZXlXaXRoU291cmNlLFxuICBnZXRBcGlLZXlGcm9tQ29uZmlnT3JNYWNPU0tleWNoYWluLFxuICBnZXRBdXRoVG9rZW5Tb3VyY2UsXG4gIGlzQ2xhdWRlQUlTdWJzY3JpYmVyLFxufSBmcm9tICcuL2F1dGguanMnXG5pbXBvcnQgdHlwZSB7IEFnZW50RGVmaW5pdGlvbnNSZXN1bHQgfSBmcm9tICcuLi90b29scy9BZ2VudFRvb2wvbG9hZEFnZW50c0Rpci5qcydcbmltcG9ydCB7XG4gIGdldEFnZW50RGVzY3JpcHRpb25zVG90YWxUb2tlbnMsXG4gIEFHRU5UX0RFU0NSSVBUSU9OU19USFJFU0hPTEQsXG59IGZyb20gJy4vc3RhdHVzTm90aWNlSGVscGVycy5qcydcbmltcG9ydCB7XG4gIGlzU3VwcG9ydGVkSmV0QnJhaW5zVGVybWluYWwsXG4gIHRvSURFRGlzcGxheU5hbWUsXG4gIGdldFRlcm1pbmFsSWRlVHlwZSxcbn0gZnJvbSAnLi9pZGUuanMnXG5pbXBvcnQgeyBpc0pldEJyYWluc1BsdWdpbkluc3RhbGxlZENhY2hlZFN5bmMgfSBmcm9tICcuL2pldGJyYWlucy5qcydcblxuLy8gVHlwZXNcbmV4cG9ydCB0eXBlIFN0YXR1c05vdGljZVR5cGUgPSAnd2FybmluZycgfCAnaW5mbydcblxuZXhwb3J0IHR5cGUgU3RhdHVzTm90aWNlQ29udGV4dCA9IHtcbiAgY29uZmlnOiBSZXR1cm5UeXBlPHR5cGVvZiBnZXRHbG9iYWxDb25maWc+XG4gIGFnZW50RGVmaW5pdGlvbnM/OiBBZ2VudERlZmluaXRpb25zUmVzdWx0XG4gIG1lbW9yeUZpbGVzOiBNZW1vcnlGaWxlSW5mb1tdXG59XG5cbmV4cG9ydCB0eXBlIFN0YXR1c05vdGljZURlZmluaXRpb24gPSB7XG4gIGlkOiBzdHJpbmdcbiAgdHlwZTogU3RhdHVzTm90aWNlVHlwZVxuICBpc0FjdGl2ZTogKGNvbnRleHQ6IFN0YXR1c05vdGljZUNvbnRleHQpID0+IGJvb2xlYW5cbiAgcmVuZGVyOiAoY29udGV4dDogU3RhdHVzTm90aWNlQ29udGV4dCkgPT4gUmVhY3QuUmVhY3ROb2RlXG59XG5cbi8vIEluZGl2aWR1YWwgbm90aWNlIGRlZmluaXRpb25zXG5jb25zdCBsYXJnZU1lbW9yeUZpbGVzTm90aWNlOiBTdGF0dXNOb3RpY2VEZWZpbml0aW9uID0ge1xuICBpZDogJ2xhcmdlLW1lbW9yeS1maWxlcycsXG4gIHR5cGU6ICd3YXJuaW5nJyxcbiAgaXNBY3RpdmU6IGN0eCA9PiBnZXRMYXJnZU1lbW9yeUZpbGVzKGN0eC5tZW1vcnlGaWxlcykubGVuZ3RoID4gMCxcbiAgcmVuZGVyOiBjdHggPT4ge1xuICAgIGNvbnN0IGxhcmdlTWVtb3J5RmlsZXMgPSBnZXRMYXJnZU1lbW9yeUZpbGVzKGN0eC5tZW1vcnlGaWxlcylcbiAgICByZXR1cm4gKFxuICAgICAgPD5cbiAgICAgICAge2xhcmdlTWVtb3J5RmlsZXMubWFwKGZpbGUgPT4ge1xuICAgICAgICA