本章总览
Doctor.tsx(约 575 行)渲染 Claude Code 安装与配置诊断全屏报告:版本、包管理器、MCP 解析警告、Agent 目录、沙箱状态、插件错误、权限规则 unreachable 警告等。由 /doctor 斜杠命令或 claude doctor CLI 挂载,完成后 onDone 返回 REPL。本章从 screens 层讲解数据收集与分区渲染;底层 diagnostic 逻辑在 utils/doctorDiagnostic.ts。
学完本章你应该能
- 说明 Doctor Props(onDone)与命令注册路径
- 描述 getDoctorDiagnostic 与并行 agent/context/lock 探测
- 理解 validationErrors、envValidationErrors、pluginsErrors 分区
- 掌握 SandboxDoctorSection 与 McpParsingWarnings 子组件接缝
- 区分 Doctor 与 REPL 内嵌 settings 错误通知的差异
核心概念(先读懂这些)
Doctor 是只读诊断页,不参与 query 循环
Doctor 不 import REPL、不持有 messages state。useExitOnCtrlCDWithKeybindings 允许 Ctrl+C 退出;confirm:yes/no 键绑定调用 onDone。这与 ResumeConversation(handoff 到 REPL)和 REPL(长生命周期)形成 screens 层三分类:主会话 / 向导 / 一次性工具页。
诊断数据分 sync 与 async 两波
distTagsPromise 在 render 时启动 getDoctorDiagnostic().then;useEffect 内并行 pathExists agents 目录、checkContextWarnings、pid lock 清理。diagnostic 未返回前显示 "Checking installation status…"。
建议学习步骤
- 阅读 export function Doctor 与 state 声明(源码块 A)
- 阅读 useEffect 数据收集(源码块 B)
- 阅读 diagnostic 就绪后的安装信息区(源码块 C)
- 阅读 Sandbox/MCP/Plugin/Context 警告区(源码块 D)
- 对照 commands/doctor 与 cli handlers 懒加载(源码块 E)
常见误区
注意
DoctorWithPlugins 在 cli/handlers/util.tsx 用 React.lazy 包裹,避免非 doctor 路径加载
注意
envValidationErrors 硬编码 BASH_MAX_OUTPUT、TASK_MAX_OUTPUT、CLAUDE_CODE_MAX_OUTPUT_TOKENS 上限
注意
pluginsErrors 来自 AppState,与 useManagePlugins 写入路径一致
注意
agentInfo.failedFiles 与 loadAgentsDir 解析失败同源,修 agent.md 后需重跑 /doctor
注意
Dismiss 后 transcript 插入 system 行
screens 层入口:命令与 CLI
Doctor 有两条挂载路径:
1. 交互 REPL 内 /doctor
commands/doctor/doctor.tsx
→ Promise.resolve(<Doctor onDone={onDone} />)
2. claude doctor(非交互/util handler)
cli/handlers/util.tsx
→ DoctorWithPlugins = React.lazy(() => import('screens/Doctor'))
→ Suspense fallback={null}
onDone 签名:(result?: string, options?: { display?: CommandResultDisplay }) => void。Dismiss 时传递 "Claude Code diagnostics dismissed" 与 display: 'system',在 transcript 插入 system 行。
Doctor 不是 Screen 枚举值;执行完毕返回 REPL prompt 视图,不修改 REPL 的 screen state。
源码引用: src/commands/doctor/doctor.tsx · 第 1–5 行(共 8 行)
1| import React from 'react'
2| import { Doctor } from '../../screens/Doctor.js'
3| import type { LocalJSXCommandCall } from '../../types/command.js'
4|
5| export const call: LocalJSXCommandCall = (onDone, _context, _args) => {
源码引用: src/cli/handlers/util.tsx · 第 52–64 行(共 127 行)
52| </Box>
53| </KeybindingSetup>
54| </AppStateProvider>,
55| )
56| })
57| root.unmount()
58| process.exit(0)
59| }
60|
61| // DoctorWithPlugins wrapper + doctor handler
62| const DoctorLazy = React.lazy(() =>
63| import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })),
64| )
Doctor 组件 state 与 AppState 订阅
Doctor 从 AppState 读取:
agentDefinitions— activeAgents、failedFilesmcpTools— 默认 [],供 checkContextWarningstoolPermissionContext— async 传入 context warningsplugins.errors— 插件加载失败列表
本地 state:
diagnostic— DiagnosticInfo(安装类型、版本、路径)agentInfo— 用户/项目 agents 目录存在性与 failedFilescontextWarnings— CLAUDE.md / agent / MCP token 占用versionLockInfo— PID lock 清理结果
useSettingsErrors() 提供 settings.json 校验错误;errorsExcludingMcp 过滤 MCP 相关项单独展示。
源码引用: src/screens/Doctor.tsx · 第 32–56 行(共 517 行)
32| } from '../utils/autoUpdater.js'
33| import {
34| type ContextWarnings,
35| checkContextWarnings,
36| } from '../utils/doctorContextWarnings.js'
37| import {
38| type DiagnosticInfo,
39| getDoctorDiagnostic,
40| } from '../utils/doctorDiagnostic.js'
41| import { validateBoundedIntEnvVar } from '../utils/envValidation.js'
42| import { pathExists } from '../utils/file.js'
43| import {
44| cleanupStaleLocks,
45| getAllLockInfo,
46| isPidBasedLockingEnabled,
47| type LockInfo,
48| } from '../utils/nativeInstaller/pidLock.js'
49| import { getInitialSettings } from '../utils/settings/settings.js'
50| import {
51| BASH_MAX_OUTPUT_DEFAULT,
52| BASH_MAX_OUTPUT_UPPER_LIMIT,
53| } from '../utils/shell/outputLimits.js'
54| import {
55| TASK_MAX_OUTPUT_DEFAULT,
56| TASK_MAX_OUTPUT_UPPER_LIMIT,
源码引用: src/screens/Doctor.tsx · 第 100–123 行(共 517 行)
100| )
101| }
102|
103| export function Doctor({ onDone }: Props): React.ReactNode {
104| const agentDefinitions = useAppState(s => s.agentDefinitions)
105| const mcpTools = useAppState(s => s.mcp.tools)
106| const toolPermissionContext = useAppState(s => s.toolPermissionContext)
107| const pluginsErrors = useAppState(s => s.plugins.errors)
108| useExitOnCtrlCDWithKeybindings()
109|
110| const tools = useMemo(() => {
111| return mcpTools || []
112| }, [mcpTools])
113|
114| const [diagnostic, setDiagnostic] = useState<DiagnosticInfo | null>(null)
115| const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null)
116| const [contextWarnings, setContextWarnings] =
117| useState<ContextWarnings | null>(null)
118| const [versionLockInfo, setVersionLockInfo] =
119| useState<VersionLockInfo | null>(null)
120| const validationErrors = useSettingsErrors()
121|
122| // Create promise once for dist-tags fetch (depends on diagnostic)
123| const distTagsPromise = useMemo(
useEffect:并行诊断收集
mount 后 useEffect 触发:
getDoctorDiagnostic().then(setDiagnostic)— npm/gcs dist tags、安装路径- 异步 IIFE:
- pathExists userAgentsDir / projectAgentsDir
checkContextWarnings(tools, { activeAgents, allAgents, failedFiles }, ...)isPidBasedLockingEnabled()时 cleanupStaleLocks + getAllLockInfo
validateBoundedIntEnvVar 检查环境变量是否在 default..upperLimit 内,生成 envValidationErrors。
distTagsPromise 同时喂给 Suspense 包裹的 DistTagsDisplay 子组件,展示 latest/stable 版本行。
源码引用: src/screens/Doctor.tsx · 第 124–161 行(共 517 行)
124| () =>
125| getDoctorDiagnostic().then(diag => {
126| const fetchDistTags =
127| diag.installationType === 'native' ? getGcsDistTags : getNpmDistTags
128| return fetchDistTags().catch(() => ({ latest: null, stable: null }))
129| }),
130| [],
131| )
132| const autoUpdatesChannel =
133| getInitialSettings()?.autoUpdatesChannel ?? 'latest'
134|
135| const errorsExcludingMcp = validationErrors.filter(
136| error => error.mcpErrorMetadata === undefined,
137| )
138|
139| const envValidationErrors = useMemo(() => {
140| const envVars = [
141| {
142| name: 'BASH_MAX_OUTPUT_LENGTH',
143| default: BASH_MAX_OUTPUT_DEFAULT,
144| upperLimit: BASH_MAX_OUTPUT_UPPER_LIMIT,
145| },
146| {
147| name: 'TASK_MAX_OUTPUT_LENGTH',
148| default: TASK_MAX_OUTPUT_DEFAULT,
149| upperLimit: TASK_MAX_OUTPUT_UPPER_LIMIT,
150| },
151| {
152| name: 'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
153| // Check for values against the latest supported model
154| ...getModelMaxOutputTokens('claude-opus-4-6'),
155| },
156| ]
157| return envVars
158| .map(v => {
159| const value = process.env[v.name]
160| const result = validateBoundedIntEnvVar(
161| v.name,
源码引用: src/screens/Doctor.tsx · 第 164–221 行(共 517 行)
164| v.upperLimit,
165| )
166| return { name: v.name, ...result }
167| })
168| .filter(v => v.status !== 'valid')
169| }, [])
170|
171| useEffect(() => {
172| void getDoctorDiagnostic().then(setDiagnostic)
173|
174| void (async () => {
175| const userAgentsDir = join(getClaudeConfigHomeDir(), 'agents')
176| const projectAgentsDir = join(getOriginalCwd(), '.claude', 'agents')
177|
178| const { activeAgents, allAgents, failedFiles } = agentDefinitions
179|
180| const [userDirExists, projectDirExists] = await Promise.all([
181| pathExists(userAgentsDir),
182| pathExists(projectAgentsDir),
183| ])
184|
185| const agentInfoData = {
186| activeAgents: activeAgents.map(a => ({
187| agentType: a.agentType,
188| source: a.source,
189| })),
190| userAgentsDir,
191| projectAgentsDir,
192| userDirExists,
193| projectDirExists,
194| failedFiles,
195| }
196| setAgentInfo(agentInfoData)
197|
198| const warnings = await checkContextWarnings(
199| tools,
200| {
201| activeAgents,
202| allAgents,
203| failedFiles,
204| },
205| async () => toolPermissionContext,
206| )
207| setContextWarnings(warnings)
208|
209| // Fetch version lock info if PID-based locking is enabled
210| if (isPidBasedLockingEnabled()) {
211| const locksDir = join(getXDGStateHome(), 'claude', 'locks')
212| const staleLocksCleaned = cleanupStaleLocks(locksDir)
213| const locks = getAllLockInfo(locksDir)
214| setVersionLockInfo({
215| enabled: true,
216| locks,
217| locksDir,
218| staleLocksCleaned,
219| })
220| } else {
221| setVersionLockInfo({
安装信息与版本区
diagnostic 就绪后渲染 Diagnostics 标题块:
- Currently running: {installationType} ({version})
- Package manager(若有)
- Path: installationPath
- Invoked binary
- Auto-update channel(来自 getInitialSettings)
- DistTagsDisplay:latest / stable(Suspense)
PressEnterToContinue 底部等待用户确认;键绑定 confirm:yes / confirm:no 均触发 handleDismiss。
loading 态:<Pane><Text dimColor>Checking installation status…</Text></Pane>。
源码引用: src/screens/Doctor.tsx · 第 256–265 行(共 517 行)
256| <Box flexDirection="column">
257| <Text bold>Diagnostics</Text>
258| <Text>
259| └ Currently running: {diagnostic.installationType} (
260| {diagnostic.version})
261| </Text>
262| {diagnostic.packageManager && (
263| <Text>└ Package manager: {diagnostic.packageManager}</Text>
264| )}
265| <Text>└ Path: {diagnostic.installationPath}</Text>
源码引用: src/screens/Doctor.tsx · 第 266–297 行(共 517 行)
266| <Text>└ Invoked: {diagnostic.invokedBinary}</Text>
267| <Text>└ Config install method: {diagnostic.configInstallMethod}</Text>
268| <Text>
269| └ Search: {diagnostic.ripgrepStatus.working ? 'OK' : 'Not working'} (
270| {diagnostic.ripgrepStatus.mode === 'embedded'
271| ? 'bundled'
272| : diagnostic.ripgrepStatus.mode === 'builtin'
273| ? 'vendor'
274| : diagnostic.ripgrepStatus.systemPath || 'system'}
275| )
276| </Text>
277|
278| {/* Show recommendation if auto-updates are disabled */}
279| {diagnostic.recommendation && (
280| <>
281| <Text></Text>
282| <Text color="warning">
283| Recommendation: {diagnostic.recommendation.split('\n')[0]}
284| </Text>
285| <Text dimColor>{diagnostic.recommendation.split('\n')[1]}</Text>
286| </>
287| )}
288|
289| {/* Show multiple installations warning */}
290| {diagnostic.multipleInstallations.length > 1 && (
291| <>
292| <Text></Text>
293| <Text color="warning">Warning: Multiple installations found</Text>
294| {diagnostic.multipleInstallations.map((install, i) => (
295| <Text key={i}>
296| └ {install.type} at {install.path}
297| </Text>
源码引用: src/screens/Doctor.tsx · 第 57–99 行(共 517 行)
57| } from '../utils/task/outputFormatting.js'
58| import { getXDGStateHome } from '../utils/xdg.js'
59|
60| type Props = {
61| onDone: (
62| result?: string,
63| options?: { display?: CommandResultDisplay },
64| ) => void
65| }
66|
67| type AgentInfo = {
68| activeAgents: Array<{
69| agentType: string
70| source: SettingSource | 'built-in' | 'plugin'
71| }>
72| userAgentsDir: string
73| projectAgentsDir: string
74| userDirExists: boolean
75| projectDirExists: boolean
76| failedFiles?: Array<{ path: string; error: string }>
77| }
78|
79| type VersionLockInfo = {
80| enabled: boolean
81| locks: LockInfo[]
82| locksDir: string
83| staleLocksCleaned: number
84| }
85|
86| function DistTagsDisplay({
87| promise,
88| }: {
89| promise: Promise<NpmDistTags>
90| }): React.ReactNode {
91| const distTags = use(promise)
92| if (!distTags.latest) {
93| return <Text dimColor>└ Failed to fetch versions</Text>
94| }
95| return (
96| <>
97| {distTags.stable && <Text>└ Stable version: {distTags.stable}</Text>}
98| <Text>└ Latest version: {distTags.latest}</Text>
99| </>
警告与错误分区
Doctor JSX 树按类别堆叠 Box:
| 分区 | 数据源 | 组件 |
|---|---|---|
| Sandbox | 沙箱服务状态 | SandboxDoctorSection |
| MCP 解析 | MCP config 警告 | McpParsingWarnings |
| 快捷键 | keybindings 冲突 | KeybindingWarnings |
| 环境变量 | envValidationErrors | 内联 map |
| Version Locks | versionLockInfo | stale locks / active locks |
| Agent Parse | agentInfo.failedFiles | error 色 |
| Plugin Errors | pluginsErrors | getPluginErrorMessage |
| Unreachable Rules | contextWarnings | permission rules |
| Context Usage | claudeMd/agent/mcp warning | figures.warning 前缀 |
ValidationErrorsList 展示 settings 校验(排除 MCP 后)。
这种分区使 screens/Doctor 保持「报告排版」,具体检测逻辑留在 utils/。
源码引用: src/screens/Doctor.tsx · 第 400–410 行(共 517 行)
400| └ Failed to parse {agentInfo.failedFiles.length} agent file(s):
401| </Text>
402| {agentInfo.failedFiles.map((file, i) => (
403| <Text key={i} dimColor>
404| {' '}└ {file.path}: {file.error}
405| </Text>
406| ))}
407| </Box>
408| )}
409|
410| {/* Plugin Errors */}
源码引用: src/screens/Doctor.tsx · 第 425–439 行(共 517 行)
425| ))}
426| </Box>
427| )}
428|
429| {/* Unreachable Permission Rules Warning */}
430| {contextWarnings?.unreachableRulesWarning && (
431| <Box flexDirection="column">
432| <Text bold color="warning">
433| Unreachable Permission Rules
434| </Text>
435| <Text>
436| └{' '}
437| <Text color="warning">
438| {figures.warning}{' '}
439| {contextWarnings.unreachableRulesWarning.message}
源码引用: src/screens/Doctor.tsx · 第 448–478 行(共 517 行)
448| )}
449|
450| {/* Context Usage Warnings */}
451| {contextWarnings &&
452| (contextWarnings.claudeMdWarning ||
453| contextWarnings.agentWarning ||
454| contextWarnings.mcpWarning) && (
455| <Box flexDirection="column">
456| <Text bold>Context Usage Warnings</Text>
457|
458| {contextWarnings.claudeMdWarning && (
459| <>
460| <Text>
461| └{' '}
462| <Text color="warning">
463| {figures.warning} {contextWarnings.claudeMdWarning.message}
464| </Text>
465| </Text>
466| <Text>{' '}└ Files:</Text>
467| {contextWarnings.claudeMdWarning.details.map((detail, i) => (
468| <Text key={i} dimColor>
469| {' '}└ {detail}
470| </Text>
471| ))}
472| </>
473| )}
474|
475| {contextWarnings.agentWarning && (
476| <>
477| <Text>
478| └{' '}
键绑定与退出
useExitOnCtrlCDWithKeybindings() — 标准 Ctrl+C 双击退出行为。
useKeybindings 注册 Confirmation 上下文:
confirm:yes → handleDismiss
confirm:no → handleDismiss
handleDismiss 调用 onDone("Claude Code diagnostics dismissed", { display: "system" })。
与 REPL 的区别:Doctor 不设置 focusedInputDialog,不占用 tool-permission overlay 焦点栈。
源码引用: src/screens/Doctor.tsx · 第 222–255 行(共 517 行)
222| enabled: false,
223| locks: [],
224| locksDir: '',
225| staleLocksCleaned: 0,
226| })
227| }
228| })()
229| }, [toolPermissionContext, tools, agentDefinitions])
230|
231| const handleDismiss = useCallback(() => {
232| onDone('Claude Code diagnostics dismissed', { display: 'system' })
233| }, [onDone])
234|
235| // Handle dismiss via keybindings (Enter, Escape, or Ctrl+C)
236| useKeybindings(
237| {
238| 'confirm:yes': handleDismiss,
239| 'confirm:no': handleDismiss,
240| },
241| { context: 'Confirmation' },
242| )
243|
244| // Loading state
245| if (!diagnostic) {
246| return (
247| <Pane>
248| <Text dimColor>Checking installation status…</Text>
249| </Pane>
250| )
251| }
252|
253| // Format the diagnostic output according to spec
254| return (
255| <Pane>
源码引用: src/screens/Doctor.tsx · 第 109–109 行(共 517 行)
109|
DistTagsDisplay 与 Suspense
Doctor 使用 React 19 use(promise) 模式(DistTagsDisplay 子组件)消费 distTagsPromise,避免在 Doctor 主组件阻塞 render。
promise 来源 getDoctorDiagnostic().then(...) 与 useEffect 内 setDiagnostic 同一 API 两次调用——一次喂 Suspense 子树展示版本行,一次写入 diagnostic state 展示安装类型。缓存应在 doctorDiagnostic 层 dedupe,避免双份 network fetch。
Suspense fallback={null} 使版本行在加载完成前不占位,减少布局跳动。
PressEnterToContinue 与 Pane 布局
Doctor 使用 Pane design-system 组件包裹内容,与 /config 等 local-jsx 命令视觉一致。
PressEnterToContinue 在报告末尾等待确认;配合 Confirmation 键绑定 context,Enter/ y / n 均可 dismiss。
长报告分区滚动由 Ink Box flexDirection column 自然堆叠;error/warning 色来自 Ink Text color prop(error/warning),非 Emoji 前缀。
源码引用: src/screens/Doctor.tsx · 第 12–14 行(共 517 行)
12| import { McpParsingWarnings } from 'src/components/mcp/McpParsingWarnings.js'
13| import { getModelMaxOutputTokens } from 'src/utils/context.js'
14| import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js'
与 main.tsx 和 update 路径
main.tsx 约 4345 行注释 "Doctor command - check installation health" 标记 CLI 路由分支。
cli/update.ts 也调用 getDoctorDiagnostic 做非 UI 更新检查——doctorDiagnostic 是共享服务,Doctor.tsx 仅是 screens 层呈现。
维护建议:新增诊断项时,先在 doctorDiagnostic / checkContextWarnings 加数据,再在 Doctor.tsx 增 Box 分区,保持 screens 文件为薄 presentation 层。
源码引用: src/utils/doctorDiagnostic.ts · 第 1–30 行(共 626 行)
1| import { execa } from 'execa'
2| import { readFile, realpath } from 'fs/promises'
3| import { homedir } from 'os'
4| import { delimiter, join, posix, win32 } from 'path'
5| import { checkGlobalInstallPermissions } from './autoUpdater.js'
6| import { isInBundledMode } from './bundledMode.js'
7| import {
8| formatAutoUpdaterDisabledReason,
9| getAutoUpdaterDisabledReason,
10| getGlobalConfig,
11| type InstallMethod,
12| } from './config.js'
13| import { getCwd } from './cwd.js'
14| import { isEnvTruthy } from './envUtils.js'
15| import { execFileNoThrow } from './execFileNoThrow.js'
16| import { getFsImplementation } from './fsOperations.js'
17| import {
18| getShellType,
19| isRunningFromLocalInstallation,
20| localInstallationExists,
21| } from './localInstaller.js'
22| import {
23| detectApk,
24| detectAsdf,
25| detectDeb,
26| detectHomebrew,
27| detectMise,
28| detectPacman,
29| detectRpm,
30| detectWinget,
源码引用: src/hooks/useManagePlugins.ts · 第 71–71 行(共 305 行)
71| // Errors are added to the errors array for user visibility in Doctor UI
本章小结与延伸
Doctor = screens 层健康检查页。改诊断项先查 doctorDiagnostic.ts;改 UI 分区查 Doctor.tsx JSX 树。Dismiss 后 onDone 写入 system 行并返回 REPL。 继续学习: