import figures from 'figures'; import { join } from 'path'; import React, { Suspense, use, useCallback, useEffect, useMemo, useState } from 'react'; import { KeybindingWarnings } from 'src/components/KeybindingWarnings.js'; import { McpParsingWarnings } from 'src/components/mcp/McpParsingWarnings.js'; import { getModelMaxOutputTokens } from 'src/utils/context.js'; import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js'; import type { SettingSource } from 'src/utils/settings/constants.js'; import { getOriginalCwd } from '../bootstrap/state.js'; import type { CommandResultDisplay } from '../commands.js'; import { Pane } from '@anthropic/ink'; import { PressEnterToContinue } from '../components/PressEnterToContinue.js'; import { SandboxDoctorSection } from '../components/sandbox/SandboxDoctorSection.js'; import { ValidationErrorsList } from '../components/ValidationErrorsList.js'; import { useSettingsErrors } from '../hooks/notifs/useSettingsErrors.js'; import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js'; import { Box, Text } from '@anthropic/ink'; import { useKeybindings } from '../keybindings/useKeybinding.js'; import { useAppState } from '../state/AppState.js'; import { getPluginErrorMessage } from '../types/plugin.js'; import { getGcsDistTags, getNpmDistTags, type NpmDistTags } from '../utils/autoUpdater.js'; import { type ContextWarnings, checkContextWarnings } from '../utils/doctorContextWarnings.js'; import { type DiagnosticInfo, getDoctorDiagnostic } from '../utils/doctorDiagnostic.js'; import { validateBoundedIntEnvVar } from '../utils/envValidation.js'; import { pathExists } from '../utils/file.js'; import { cleanupStaleLocks, getAllLockInfo, isPidBasedLockingEnabled, type LockInfo, } from '../utils/nativeInstaller/pidLock.js'; import { getInitialSettings } from '../utils/settings/settings.js'; import { BASH_MAX_OUTPUT_DEFAULT, BASH_MAX_OUTPUT_UPPER_LIMIT } from '../utils/shell/outputLimits.js'; import { TASK_MAX_OUTPUT_DEFAULT, TASK_MAX_OUTPUT_UPPER_LIMIT } from '../utils/task/outputFormatting.js'; import { getXDGStateHome } from '../utils/xdg.js'; type Props = { onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void; }; type AgentInfo = { activeAgents: Array<{ agentType: string; source: SettingSource | 'built-in' | 'plugin'; }>; userAgentsDir: string; projectAgentsDir: string; userDirExists: boolean; projectDirExists: boolean; failedFiles?: Array<{ path: string; error: string }>; }; type VersionLockInfo = { enabled: boolean; locks: LockInfo[]; locksDir: string; staleLocksCleaned: number; }; function DistTagsDisplay({ promise }: { promise: Promise }): React.ReactNode { const distTags = use(promise); if (!distTags.latest) { return └ Failed to fetch versions; } return ( <> {distTags.stable && └ Stable version: {distTags.stable}} └ Latest version: {distTags.latest} ); } export function Doctor({ onDone }: Props): React.ReactNode { const agentDefinitions = useAppState(s => s.agentDefinitions); const mcpTools = useAppState(s => s.mcp.tools); const toolPermissionContext = useAppState(s => s.toolPermissionContext); const pluginsErrors = useAppState(s => s.plugins.errors); useExitOnCtrlCDWithKeybindings(); const tools = useMemo(() => { return mcpTools || []; }, [mcpTools]); const [diagnostic, setDiagnostic] = useState(null); const [agentInfo, setAgentInfo] = useState(null); const [contextWarnings, setContextWarnings] = useState(null); const [versionLockInfo, setVersionLockInfo] = useState(null); const validationErrors = useSettingsErrors(); // Create promise once for dist-tags fetch (depends on diagnostic) const distTagsPromise = useMemo( () => getDoctorDiagnostic().then(diag => { const fetchDistTags = diag.installationType === 'native' ? getGcsDistTags : getNpmDistTags; return fetchDistTags().catch(() => ({ latest: null, stable: null })); }), [], ); const autoUpdatesChannel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; const errorsExcludingMcp = validationErrors.filter(error => error.mcpErrorMetadata === undefined); const envValidationErrors = useMemo(() => { const envVars = [ { name: 'BASH_MAX_OUTPUT_LENGTH', default: BASH_MAX_OUTPUT_DEFAULT, upperLimit: BASH_MAX_OUTPUT_UPPER_LIMIT, }, { name: 'TASK_MAX_OUTPUT_LENGTH', default: TASK_MAX_OUTPUT_DEFAULT, upperLimit: TASK_MAX_OUTPUT_UPPER_LIMIT, }, { name: 'CLAUDE_CODE_MAX_OUTPUT_TOKENS', // Check for values against the latest supported model ...getModelMaxOutputTokens('claude-opus-4-7'), }, ]; return envVars .map(v => { const value = process.env[v.name]; const result = validateBoundedIntEnvVar(v.name, value, v.default, v.upperLimit); return { name: v.name, ...result }; }) .filter(v => v.status !== 'valid'); }, []); useEffect(() => { void getDoctorDiagnostic().then(setDiagnostic); void (async () => { const userAgentsDir = join(getClaudeConfigHomeDir(), 'agents'); const projectAgentsDir = join(getOriginalCwd(), '.claude', 'agents'); const { activeAgents, allAgents, failedFiles } = agentDefinitions; const [userDirExists, projectDirExists] = await Promise.all([ pathExists(userAgentsDir), pathExists(projectAgentsDir), ]); const agentInfoData = { activeAgents: activeAgents.map(a => ({ agentType: a.agentType, source: a.source, })), userAgentsDir, projectAgentsDir, userDirExists, projectDirExists, failedFiles, }; setAgentInfo(agentInfoData); const warnings = await checkContextWarnings( tools, { activeAgents, allAgents, failedFiles, }, async () => toolPermissionContext, ); setContextWarnings(warnings); // Fetch version lock info if PID-based locking is enabled if (isPidBasedLockingEnabled()) { const locksDir = join(getXDGStateHome(), 'claude', 'locks'); const staleLocksCleaned = cleanupStaleLocks(locksDir); const locks = getAllLockInfo(locksDir); setVersionLockInfo({ enabled: true, locks, locksDir, staleLocksCleaned, }); } else { setVersionLockInfo({ enabled: false, locks: [], locksDir: '', staleLocksCleaned: 0, }); } })(); }, [toolPermissionContext, tools, agentDefinitions]); const handleDismiss = useCallback(() => { onDone('Claude Code diagnostics dismissed', { display: 'system' }); }, [onDone]); // Handle dismiss via keybindings (Enter, Escape, or Ctrl+C) useKeybindings( { 'confirm:yes': handleDismiss, 'confirm:no': handleDismiss, }, { context: 'Confirmation' }, ); // Loading state if (!diagnostic) { return ( Checking installation status… ); } // Format the diagnostic output according to spec return ( Diagnostics └ Currently running: {diagnostic.installationType} ({diagnostic.version}) {diagnostic.packageManager && └ Package manager: {diagnostic.packageManager}} └ Path: {diagnostic.installationPath} └ Invoked: {diagnostic.invokedBinary} └ Config install method: {diagnostic.configInstallMethod} └ Search: {diagnostic.ripgrepStatus.working ? 'OK' : 'Not working'} ( {diagnostic.ripgrepStatus.mode === 'embedded' ? 'bundled' : diagnostic.ripgrepStatus.mode === 'builtin' ? 'vendor' : diagnostic.ripgrepStatus.systemPath || 'system'} ) {diagnostic.ripgrepStatus.note && └ Note: {diagnostic.ripgrepStatus.note}} {/* Show recommendation if auto-updates are disabled */} {diagnostic.recommendation && ( <> Recommendation: {diagnostic.recommendation.split('\n')[0]} {diagnostic.recommendation.split('\n')[1]} )} {/* Show multiple installations warning */} {diagnostic.multipleInstallations.length > 1 && ( <> Warning: Multiple installations found {diagnostic.multipleInstallations.map((install, i) => ( └ {install.type} at {install.path} ))} )} {/* Show configuration warnings */} {diagnostic.warnings.length > 0 && ( <> {diagnostic.warnings.map((warning, i) => ( Warning: {warning.issue} Fix: {warning.fix} ))} )} {/* Show invalid settings errors */} {errorsExcludingMcp.length > 0 && ( Invalid Settings )} {/* Updates section */} Updates └ Auto-updates: {diagnostic.packageManager ? 'Managed by package manager' : diagnostic.autoUpdates} {diagnostic.hasUpdatePermissions !== null && ( └ Update permissions: {diagnostic.hasUpdatePermissions ? 'Yes' : 'No (requires sudo)'} )} └ Auto-update channel: {autoUpdatesChannel} {/* Environment Variables */} {envValidationErrors.length > 0 && ( Environment Variables {envValidationErrors.map((validation, i) => ( └ {validation.name}:{' '} {validation.message} ))} )} {/* Version Locks (PID-based locking) */} {versionLockInfo?.enabled && ( Version Locks {versionLockInfo.staleLocksCleaned > 0 && ( └ Cleaned {versionLockInfo.staleLocksCleaned} stale lock(s) )} {versionLockInfo.locks.length === 0 ? ( └ No active version locks ) : ( versionLockInfo.locks.map((lock, i) => ( └ {lock.version}: PID {lock.pid}{' '} {lock.isProcessRunning ? (running) : (stale)} )) )} )} {agentInfo?.failedFiles && agentInfo.failedFiles.length > 0 && ( Agent Parse Errors └ Failed to parse {agentInfo.failedFiles.length} agent file(s): {agentInfo.failedFiles.map((file, i) => ( {' '}└ {file.path}: {file.error} ))} )} {/* Plugin Errors */} {pluginsErrors.length > 0 && ( Plugin Errors └ {pluginsErrors.length} plugin error(s) detected: {pluginsErrors.map((error, i) => ( {' '}└ {error.source || 'unknown'} {'plugin' in error && error.plugin ? ` [${error.plugin}]` : ''}: {getPluginErrorMessage(error)} ))} )} {/* Unreachable Permission Rules Warning */} {contextWarnings?.unreachableRulesWarning && ( Unreachable Permission Rules └{' '} {figures.warning} {contextWarnings.unreachableRulesWarning.message} {contextWarnings.unreachableRulesWarning.details.map((detail, i) => ( {' '}└ {detail} ))} )} {/* Context Usage Warnings */} {contextWarnings && (contextWarnings.claudeMdWarning || contextWarnings.agentWarning || contextWarnings.mcpWarning) && ( Context Usage Warnings {contextWarnings.claudeMdWarning && ( <> └{' '} {figures.warning} {contextWarnings.claudeMdWarning.message} {' '}└ Files: {contextWarnings.claudeMdWarning.details.map((detail, i) => ( {' '}└ {detail} ))} )} {contextWarnings.agentWarning && ( <> └{' '} {figures.warning} {contextWarnings.agentWarning.message} {' '}└ Top contributors: {contextWarnings.agentWarning.details.map((detail, i) => ( {' '}└ {detail} ))} )} {contextWarnings.mcpWarning && ( <> └{' '} {figures.warning} {contextWarnings.mcpWarning.message} {' '}└ MCP servers: {contextWarnings.mcpWarning.details.map((detail, i) => ( {' '}└ {detail} ))} )} )} ); }