Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

hooks/notifs/ 子目录(17 个文件)是 REPL 顶部横幅通知的「传感器层」:每个 Hook 监听一种产品状态(MCP 失败、settings 校验、rate limit、插件安装…),调用 context/notifications.tsx 的 addNotification 写入 AppState 队列。它们不渲染 UI——NotificationBanner 组件读取 notifications.current 显示。本章要求你理解 priority / key / fold / invalidates 契约,以及 useStartupNotification 如何统一 remote-mode 与 once-per-session 守卫。

学完本章你应该能

  • 解释 Notification 联合类型(text vs jsx)与 priority 四级
  • 说明 addNotification 对 immediate 与非 immediate 的不同路径
  • 列举 REPL.tsx import 的主要 notifs Hook 及其触发条件
  • 理解 useStartupNotification 的 compute 函数与 async 错误处理
  • 分析 useMcpConnectivityStatus 如何分 local vs claude.ai 四类通知
  • 能在 useSettingsErrors 中看到 removeNotification 的「问题消失即撤横幅」模式

核心概念(先读懂这些)

key 是幂等与更新的锚点

同一 key 的通知通常表示「同一类状态」。settings-errors 在 errors 清空时 removeNotification;MCP 失败用固定 key mcp-failed 便于重复 mount 时 fold 或替换。immediate priority 会打断当前显示并清 timeout,适合 limit-reached 等必须立刻看到的告警。

remote mode 统一静默

多数 notifs Hook 首行检查 getIsRemoteMode()——远程/headless 会话无 Ink 横幅,避免无 UI 时写 AppState。useSettingsErrors、useMcpConnectivityStatus、useRateLimitWarningNotification 等均遵循此模式。

零 UI Hook 模式

notifs/ 下 Hook 返回值多为 void 或少量状态(如 useSettingsErrors 返回 errors 供 Doctor 复用)。REPL 主体 JSX 只需在根部调用一遍 useInstallMessages() 等副作用 Hook,无需传 props。新增通知类型时复制「useEffect + addNotification + key」模式,优先复用 useStartupNotification 若仅 mount 时跑一次。

建议学习步骤

  1. 阅读 context/notifications.tsx 类型与 addNotification 分支
  2. 读 useStartupNotification 注释与 hasRunRef 守卫
  3. 打开 useSettingsErrors 看 settings 变更订阅
  4. 分析 useMcpConnectivityStatus 四个 filter 与四类 key
  5. 浏览 REPL.tsx 中 notifs import 列表
  6. 对照 useCanUseTool deny 分支的 addNotification 理解跨模块写入

常见误区

注意

addNotification immediate 路径必须 return,否则会双入队

注意

fold 回调需返回带 fold 字段的 merged 对象以支持连续合并

注意

useMcpConnectivityStatus 对 claude.ai 仅通知「曾经连通过」的 failed/needs-auth,避免 nag 新 connector

注意

不要把 utils/hooks.ts Shell Hook 与 notifs Hook 混淆——后者只管 UI 横幅

通知子系统在架构中的位置

各业务 Hook(notifs/*、useCanUseTool、PromptInput…)
  → useNotifications().addNotification / removeNotification
       ↓
AppState.notifications { current, queue }
       ↓
processQueue(priority + timeoutMs 默认 8000ms)
       ↓
NotificationBanner / Ink Text 渲染 current

REPL.tsx 在组件体顶部集中调用十余个 notifs Hook——它们彼此独立,仅共享 notifications context。Doctor.tsx 复用 useSettingsErrors 获取 errors 数组,说明同一 Hook 可同时服务横幅与别的 UI。

Notification 类型与 addNotification 核心逻辑

类型:

  • TextNotification — text + 可选 color(Theme key)
  • JSXNotification — jsx ReactNode(如 MCP 横幅带 dimColor 的 · /mcp 后缀)

Base 字段: key、priority(low|medium|high|immediate)、timeoutMs、invalidates[]、fold()

immediate 路径:

  1. 清 currentTimeoutId
  2. 立刻 setAppState current = notif
  3. 旧 current 与非 immediate 队列项重新入队(被 invalidates 的 key 除外)
  4. return — 不走普通入队

非 immediate 路径:

  • 若 notif.fold 且 key 与 current 或 queue 中某项相同,执行 fold 合并并重置 timeout
  • 否则按 priority 插入 queue,processQueue 在 current 为空时取出下一项

模块级 currentTimeoutId 保证同一时间只有一个 auto-dismiss 计时器。

源码引用: src/context/notifications.tsx · 第 5–34 行(共 313 行)

   5| 
   6| type Priority = 'low' | 'medium' | 'high' | 'immediate'
   7| 
   8| type BaseNotification = {
   9|   key: string
  10|   /**
  11|    * Keys of notifications that this notification invalidates.
  12|    * If a notification is invalidated, it will be removed from the queue
  13|    * and, if currently displayed, cleared immediately.
  14|    */
  15|   invalidates?: string[]
  16|   priority: Priority
  17|   timeoutMs?: number
  18|   /**
  19|    * Combine notifications with the same key, like Array.reduce().
  20|    * Called as fold(accumulator, incoming) when a notification with a matching
  21|    * key already exists in the queue or is currently displayed.
  22|    * Returns the merged notification (should carry fold forward for future merges).
  23|    */
  24|   fold?: (accumulator: Notification, incoming: Notification) => Notification
  25| }
  26| 
  27| type TextNotification = BaseNotification & {
  28|   text: string
  29|   color?: keyof Theme
  30| }
  31| 
  32| type JSXNotification = BaseNotification & {
  33|   jsx: React.ReactNode
  34| }

源码引用: src/context/notifications.tsx · 第 78–117 行(共 313 行)

  78|         },
  79|         next.timeoutMs ?? DEFAULT_TIMEOUT_MS,
  80|         setAppState,
  81|         next.key,
  82|         processQueue,
  83|       )
  84| 
  85|       return {
  86|         ...prev,
  87|         notifications: {
  88|           queue: prev.notifications.queue.filter(_ => _ !== next),
  89|           current: next,
  90|         },
  91|       }
  92|     })
  93|   }, [setAppState])
  94| 
  95|   const addNotification = useCallback<AddNotificationFn>(
  96|     (notif: Notification) => {
  97|       // Handle immediate priority notifications
  98|       if (notif.priority === 'immediate') {
  99|         // Clear any existing timeout since we're showing a new immediate notification
 100|         if (currentTimeoutId) {
 101|           clearTimeout(currentTimeoutId)
 102|           currentTimeoutId = null
 103|         }
 104| 
 105|         // Set up timeout for the immediate notification
 106|         currentTimeoutId = setTimeout(
 107|           (setAppState, notif, processQueue) => {
 108|             currentTimeoutId = null
 109|             setAppState(prev => {
 110|               // Compare by key instead of reference to handle re-created notifications
 111|               if (prev.notifications.current?.key !== notif.key) {
 112|                 return prev
 113|               }
 114|               return {
 115|                 ...prev,
 116|                 notifications: {
 117|                   queue: prev.notifications.queue.filter(

源码引用: src/context/notifications.tsx · 第 119–164 行(共 313 行)

 119|                   ),
 120|                   current: null,
 121|                 },
 122|               }
 123|             })
 124|             processQueue()
 125|           },
 126|           notif.timeoutMs ?? DEFAULT_TIMEOUT_MS,
 127|           setAppState,
 128|           notif,
 129|           processQueue,
 130|         )
 131| 
 132|         // Show the immediate notification right away
 133|         setAppState(prev => ({
 134|           ...prev,
 135|           notifications: {
 136|             current: notif,
 137|             queue:
 138|               // Only re-queue the current notification if it's not immediate
 139|               [
 140|                 ...(prev.notifications.current
 141|                   ? [prev.notifications.current]
 142|                   : []),
 143|                 ...prev.notifications.queue,
 144|               ].filter(
 145|                 _ =>
 146|                   _.priority !== 'immediate' &&
 147|                   !notif.invalidates?.includes(_.key),
 148|               ),
 149|           },
 150|         }))
 151|         return // IMPORTANT: Exit addNotification for immediate notifications
 152|       }
 153| 
 154|       // Handle non-immediate notifications
 155|       setAppState(prev => {
 156|         // Check if we can fold into an existing notification with the same key
 157|         if (notif.fold) {
 158|           // Fold into current notification if keys match
 159|           if (prev.notifications.current?.key === notif.key) {
 160|             const folded = notif.fold(prev.notifications.current, notif)
 161|             // Reset timeout for the folded notification
 162|             if (currentTimeoutId) {
 163|               clearTimeout(currentTimeoutId)
 164|               currentTimeoutId = null

useStartupNotification:mount 一次模板

hooks/notifs/ 里大量 Hook 只需在 会话首次 mount 时发通知(安装检查结果、迁移提示等)。useStartupNotification(compute) 封装:

  1. getIsRemoteMode() → 跳过
  2. hasRunRef → 只跑一次
  3. Promise.resolve().then(() =&gt; computeRef.current()) — sync/async 皆可
  4. result 为 null 跳过;Notification 或 Notification[] 逐个 addNotification
  5. reject → logError

compute 通过 ref 持有最新闭包,但 effect 只跑一次——compute 内不应依赖后续变化的 props(除非故意只读 mount 快照)。

useInstallMessages 是典型 consumer:async checkInstall(),按 message.type 映射 priority(error/userActionRequired → high,path/alias → medium,其余 low)与 color。

源码引用: src/hooks/notifs/useStartupNotification.ts · 第 11–41 行(共 42 行)

  11| /**
  12|  * Fires notification(s) once on mount. Encapsulates the remote-mode gate and
  13|  * once-per-session ref guard that was hand-rolled across 10+ notifs/ hooks.
  14|  *
  15|  * The compute fn runs exactly once on first effect. Return null to skip,
  16|  * a Notification to fire one, or an array to fire several. Sync or async.
  17|  * Rejections are routed to logError.
  18|  */
  19| export function useStartupNotification(
  20|   compute: () => Result | Promise<Result>,
  21| ): void {
  22|   const { addNotification } = useNotifications()
  23|   const hasRunRef = useRef(false)
  24|   const computeRef = useRef(compute)
  25|   computeRef.current = compute
  26| 
  27|   useEffect(() => {
  28|     if (getIsRemoteMode() || hasRunRef.current) return
  29|     hasRunRef.current = true
  30| 
  31|     void Promise.resolve()
  32|       .then(() => computeRef.current())
  33|       .then(result => {
  34|         if (!result) return
  35|         for (const n of Array.isArray(result) ? result : [result]) {
  36|           addNotification(n)
  37|         }
  38|       })
  39|       .catch(logError)
  40|   }, [addNotification])
  41| }

源码引用: src/hooks/notifs/useInstallMessages.tsx · 第 1–23 行(共 23 行)

   1| import { checkInstall } from 'src/utils/nativeInstaller/index.js'
   2| import { useStartupNotification } from './useStartupNotification.js'
   3| 
   4| export function useInstallMessages(): void {
   5|   useStartupNotification(async () => {
   6|     const messages = await checkInstall()
   7|     return messages.map((message, index) => {
   8|       let priority: 'low' | 'medium' | 'high' | 'immediate' = 'low'
   9|       if (message.type === 'error' || message.userActionRequired) {
  10|         priority = 'high'
  11|       } else if (message.type === 'path' || message.type === 'alias') {
  12|         priority = 'medium'
  13|       }
  14|       return {
  15|         key: `install-message-${index}-${message.type}`,
  16|         text: message.message,
  17|         priority,
  18|         color: message.type === 'error' ? 'error' : 'warning',
  19|       }
  20|     })
  21|   })
  22| }
  23| 

useSettingsErrors:持久态横幅 + Doctor 复用

useSettingsErrors 模式代表 「状态存在则显示,消失则 remove」:

  1. useState 初始值来自 getSettingsWithAllErrors()
  2. useSettingsChange 回调刷新 errors
  3. useEffect:remote mode 跳过;errors.length > 0 则 addNotification(key: settings-errors,warning,high,60s timeout);否则 removeNotification

返回 errors 数组供 Doctor 页面展示详情,横幅仅提示「N settings issues · /doctor」。

对比 startup 类 Hook: settings 错误可能用户修复 settings 文件后消失,必须 removeNotification 而非等 timeout。

源码引用: src/hooks/notifs/useSettingsErrors.tsx · 第 8–42 行(共 42 行)

   8| const SETTINGS_ERRORS_NOTIFICATION_KEY = 'settings-errors'
   9| 
  10| export function useSettingsErrors(): ValidationError[] {
  11|   const { addNotification, removeNotification } = useNotifications()
  12|   const [errors, setErrors] = useState<ValidationError[]>(() => {
  13|     const { errors } = getSettingsWithAllErrors()
  14|     return errors
  15|   })
  16| 
  17|   const handleSettingsChange = useCallback(() => {
  18|     const { errors } = getSettingsWithAllErrors()
  19|     setErrors(errors)
  20|   }, [])
  21| 
  22|   useSettingsChange(handleSettingsChange)
  23| 
  24|   useEffect(() => {
  25|     if (getIsRemoteMode()) return
  26|     if (errors.length > 0) {
  27|       const message = `Found ${errors.length} settings ${errors.length === 1 ? 'issue' : 'issues'} · /doctor for details`
  28|       addNotification({
  29|         key: SETTINGS_ERRORS_NOTIFICATION_KEY,
  30|         text: message,
  31|         color: 'warning',
  32|         priority: 'high',
  33|         timeoutMs: 60000,
  34|       })
  35|     } else {
  36|       removeNotification(SETTINGS_ERRORS_NOTIFICATION_KEY)
  37|     }
  38|   }, [errors, addNotification, removeNotification])
  39| 
  40|   return errors
  41| }
  42| 

useMcpConnectivityStatus:四分桶 MCP 告警

传入 mcpClients(默认 []),useEffect 内过滤四类:

过滤key颜色
local failed(排除 sse-ide/ws-ide/claudeai-proxy)mcp-failederror
claude.ai failed 且 hasClaudeAiMcpEverConnectedmcp-claudeai-failederror
local needs-authmcp-needs-authwarning
claude.ai needs-auth 且曾连接mcp-claudeai-needs-authwarning

注释解释 claude.ai 策略:全新 connector 的 needs-auth 不 nag;曾经连上现在失败才 worth 横幅(状态变化 worthy)。

jsx 通知用 Ink Text 组合主文案 + dim 「· /mcp」引导用户进入管理面板。依赖 useMergedClients 提供的合并客户端列表。

源码引用: src/hooks/notifs/useMcpConnectivityStatus.tsx · 第 12–75 行(共 127 行)

  12| 
  13| const EMPTY_MCP_CLIENTS: MCPServerConnection[] = []
  14| 
  15| export function useMcpConnectivityStatus({
  16|   mcpClients = EMPTY_MCP_CLIENTS,
  17| }: Props): void {
  18|   const { addNotification } = useNotifications()
  19|   useEffect(() => {
  20|     if (getIsRemoteMode()) return
  21|     const failedLocalClients = mcpClients.filter(
  22|       client =>
  23|         client.type === 'failed' &&
  24|         client.config.type !== 'sse-ide' &&
  25|         client.config.type !== 'ws-ide' &&
  26|         client.config.type !== 'claudeai-proxy',
  27|     )
  28|     // claude.ai failures get a separate notification: they almost always indicate
  29|     // a toolbox-service outage (shared auth backend), not a local config issue.
  30|     // Only flag connectors that have previously connected successfully — an
  31|     // org-configured connector that's been needs-auth since it appeared is one
  32|     // the user has ignored and shouldn't nag about; one that was working
  33|     // yesterday and is now failed is a state change worth surfacing.
  34|     const failedClaudeAiClients = mcpClients.filter(
  35|       client =>
  36|         client.type === 'failed' &&
  37|         client.config.type === 'claudeai-proxy' &&
  38|         hasClaudeAiMcpEverConnected(client.name),
  39|     )
  40|     const needsAuthLocalServers = mcpClients.filter(
  41|       client =>
  42|         client.type === 'needs-auth' && client.config.type !== 'claudeai-proxy',
  43|     )
  44|     const needsAuthClaudeAiServers = mcpClients.filter(
  45|       client =>
  46|         client.type === 'needs-auth' &&
  47|         client.config.type === 'claudeai-proxy' &&
  48|         hasClaudeAiMcpEverConnected(client.name),
  49|     )
  50|     if (
  51|       failedLocalClients.length === 0 &&
  52|       failedClaudeAiClients.length === 0 &&
  53|       needsAuthLocalServers.length === 0 &&
  54|       needsAuthClaudeAiServers.length === 0
  55|     ) {
  56|       return
  57|     }
  58|     if (failedLocalClients.length > 0) {
  59|       addNotification({
  60|         key: 'mcp-failed',
  61|         jsx: (
  62|           <>
  63|             <Text color="error">
  64|               {failedLocalClients.length} MCP{' '}
  65|               {failedLocalClients.length === 1 ? 'server' : 'servers'} failed
  66|             </Text>
  67|             <Text dimColor> · /mcp</Text>
  68|           </>
  69|         ),
  70|         priority: 'medium',
  71|       })
  72|     }
  73|     if (failedClaudeAiClients.length > 0) {
  74|       addNotification({
  75|         key: 'mcp-claudeai-failed',

源码引用: src/hooks/notifs/useMcpConnectivityStatus.tsx · 第 76–87 行(共 127 行)

  76|         jsx: (
  77|           <>
  78|             <Text color="error">
  79|               {failedClaudeAiClients.length} claude.ai{' '}
  80|               {failedClaudeAiClients.length === 1 ? 'connector' : 'connectors'}{' '}
  81|               unavailable
  82|             </Text>
  83|             <Text dimColor> · /mcp</Text>
  84|           </>
  85|         ),
  86|         priority: 'medium',
  87|       })

useRateLimitWarningNotification 与 immediate 优先级

rate limit / overage 类通知使用 priority: immediate,打断当前横幅立刻显示 limit-reached。

逻辑要点:

  • useClaudeAiLimits() 提供配额状态
  • getRateLimitWarning / getUsingOverageText 生成文案
  • isUsingOverage 时 addNotification immediate;恢复后 reset hasShownOverageNotification
  • team/enterprise 需 hasClaudeAiBillingAccess 才显示 overage

immediate 与 settings-errors(high 但非 immediate)对比,理解何时抢焦点 vs 排队。

源码引用: src/hooks/notifs/useRateLimitWarningNotification.tsx · 第 11–74 行(共 81 行)

  11| import { hasClaudeAiBillingAccess } from 'src/utils/billing.js'
  12| import { getIsRemoteMode } from '../../bootstrap/state.js'
  13| 
  14| export function useRateLimitWarningNotification(model: string): void {
  15|   const { addNotification } = useNotifications()
  16|   const claudeAiLimits = useClaudeAiLimits()
  17|   // claudeAiLimits reference is stable until statusListeners fire (API
  18|   // response), so these skip the Intl formatting work on most REPL renders.
  19|   const rateLimitWarning = useMemo(
  20|     () => getRateLimitWarning(claudeAiLimits, model),
  21|     [claudeAiLimits, model],
  22|   )
  23|   const usingOverageText = useMemo(
  24|     () => getUsingOverageText(claudeAiLimits),
  25|     [claudeAiLimits],
  26|   )
  27|   const shownWarningRef = useRef<string | null>(null)
  28|   const subscriptionType = getSubscriptionType()
  29|   const hasBillingAccess = hasClaudeAiBillingAccess()
  30|   const isTeamOrEnterprise =
  31|     subscriptionType === 'team' || subscriptionType === 'enterprise'
  32| 
  33|   // Track overage mode transitions
  34|   const [hasShownOverageNotification, setHasShownOverageNotification] =
  35|     useState(false)
  36| 
  37|   // Show immediate notification when entering overage mode
  38|   useEffect(() => {
  39|     if (getIsRemoteMode()) return
  40|     if (
  41|       claudeAiLimits.isUsingOverage &&
  42|       !hasShownOverageNotification &&
  43|       (!isTeamOrEnterprise || hasBillingAccess)
  44|     ) {
  45|       addNotification({
  46|         key: 'limit-reached',
  47|         text: usingOverageText,
  48|         priority: 'immediate',
  49|       })
  50|       setHasShownOverageNotification(true)
  51|     } else if (!claudeAiLimits.isUsingOverage && hasShownOverageNotification) {
  52|       // Reset when no longer in overage mode
  53|       setHasShownOverageNotification(false)
  54|     }
  55|   }, [
  56|     claudeAiLimits.isUsingOverage,
  57|     usingOverageText,
  58|     hasShownOverageNotification,
  59|     addNotification,
  60|     hasBillingAccess,
  61|     isTeamOrEnterprise,
  62|   ])
  63| 
  64|   // Show warning notification for approaching limits
  65|   useEffect(() => {
  66|     if (getIsRemoteMode()) return
  67|     if (rateLimitWarning && rateLimitWarning !== shownWarningRef.current) {
  68|       shownWarningRef.current = rateLimitWarning
  69|       addNotification({
  70|         key: 'rate-limit-warning',
  71|         jsx: (
  72|           <Text>
  73|             <Text color="warning">{rateLimitWarning}</Text>
  74|           </Text>

REPL 挂载的 notifs Hook 清单

REPL.tsx 导入的主要传感器(按产品域分组):

安装与环境: useInstallMessages、useNpmDeprecationNotification、useDeprecationWarningNotification

账户与限额: useRateLimitWarningNotification、useCanSwitchToExistingSubscription、useAutoModeUnavailableNotification、useFastModeNotification

MCP / IDE / LSP: useMcpConnectivityStatus、useIDEStatusIndicator、useLspInitializationNotification

插件: usePluginInstallationStatus、usePluginAutoupdateNotification

设置与迁移: useSettingsErrors、useModelMigrationNotifications

协作: useTeammateLifecycleNotification(teammate shutdown)、useAntOrgWarningNotification(ANT 构建专用)

每个 Hook 文件通常 <100 行,模式高度一致。读一个即可类推其余;差异在业务判断与 key 命名。

源码引用: src/screens/REPL.tsx · 第 232–267 行(共 7050 行)

 232|   applyPermissionUpdate,
 233|   applyPermissionUpdates,
 234|   persistPermissionUpdate,
 235| } from '../utils/permissions/PermissionUpdate.js'
 236| import { buildPermissionUpdates } from '../components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'
 237| import { stripDangerousPermissionsForAutoMode } from '../utils/permissions/permissionSetup.js'
 238| import {
 239|   getScratchpadDir,
 240|   isScratchpadEnabled,
 241| } from '../utils/permissions/filesystem.js'
 242| import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js'
 243| import { SLEEP_TOOL_NAME } from '../tools/SleepTool/prompt.js'
 244| import { clearSpeculativeChecks } from '../tools/BashTool/bashPermissions.js'
 245| import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
 246| import {
 247|   getGlobalConfig,
 248|   saveGlobalConfig,
 249|   getGlobalConfigWriteCount,
 250| } from '../utils/config.js'
 251| import { hasConsoleBillingAccess } from '../utils/billing.js'
 252| import {
 253|   logEvent,
 254|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 255| } from 'src/services/analytics/index.js'
 256| import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
 257| import {
 258|   textForResubmit,
 259|   handleMessageFromStream,
 260|   type StreamingToolUse,
 261|   type StreamingThinking,
 262|   isCompactBoundaryMessage,
 263|   getMessagesAfterCompactBoundary,
 264|   getContentText,
 265|   createUserMessage,
 266|   createAssistantMessage,
 267|   createTurnDurationMessage,

与其他模块的交叉写入

notifs 队列不只 hooks/notifs/ 写入:

  • useCanUseTool deny 分支:auto-mode classifier 拒绝时 addNotification 提示 /permissions
  • PromptInput:think/ultraplan trigger、粘贴过大等场景 addNotification
  • query/stopHooks:Stop hook 失败可选 addNotification
  • buddy/useBuddyNotification:Buddy 功能专用横幅

调试「横幅从哪来」时,全局搜 addNotification({ key: 并对照 key 字符串。hooks/notifs/ 应使用稳定、文档化的 key; ad-hoc 通知也应用 key 便于去重。

源码目录(notifs 子目录)

核心基础设施:context/notifications.tsx。渲染侧在 components 中搜索 NotificationBanner 或 notifications.current。

动手练习

  1. 故意写错 settings.json,确认 settings-errors 横幅出现;修复后横幅消失
  2. 断开 MCP server,观察 mcp-failed jsx 文案与 /mcp 引导
  3. 在 addNotification 设 breakpoint,区分 immediate 与普通入队路径
  4. 阅读 usePluginAutoupdateNotification,对比 useStartupNotification 与 useEffect 周期检查差异
  5. 对照 useCanUseTool deny 通知,画出权限拒绝到横幅的完整链

本章小结与延伸

notifs Hook = 把分散的产品状态翻译成统一 Notification 队列。下一章可回到 useCanUseTool,看 deny 分支如何 addNotification 提示 /permissions。 继续学习:

  • hooks 模块总览
  • useCanUseTool
Prev
合并态 Hook(MCP + 本地)
Next
repl-bridge · REPL 桥初始化与传输