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

本章总览

本章拆解 state/ 核心三件套:store.ts(约 35 行)实现通用订阅存储;AppStateStore.ts(约 570 行)定义 AppState 巨型类型与 getDefaultAppState;AppState.tsx(约 200 行)提供 AppStateProvider、useAppState / useSetAppState / useAppStateStore。读完后应能解释:为何 Provider 用稳定 store 实例、为何 selector 不能返回新对象、以及 getDefaultAppState 里 lazy require teammate 的原因。

学完本章你应该能

  • 说明 createStore 的 Object.is 短路语义
  • 列举 AppState 主要字段簇及其消费方
  • 描述 AppStateProvider 挂载时的 bypass 权限修正
  • 理解 useSettingsChange 如何把磁盘 settings 推入 AppState
  • 区分 useAppState 与 useAppStateMaybeOutsideOfProvider 的使用场景
  • 能在 main.tsx 找到 headless createStore 与 onChangeAppState 接线

核心概念(先读懂这些)

store 是框架无关的最小内核

createStore 不依赖 React。headless SDK、MCP entrypoint、print.ts 可直接 createStore(getDefaultAppState(), onChangeAppState) 获得与 REPL 相同的 setState 语义。subscribe 是 Set,每次 setState 遍历通知——Ink 组件通过 useSyncExternalStore 桥接。

AppState 类型即产品状态机文档

AppStateStore.ts 内联注释密度极高:每个字段说明写入者、读者、是否持久化。例如 replBridge* 七字段描述 always-on bridge 状态机;teamContext 区分 swarm 多进程与 toolUseContext.agentId 的 in-process subagent。改字段前先读注释,避免把 session-only 数据误 persist。

Provider 嵌套禁止

HasAppStateContext 标记防止 AppStateProvider 嵌套——嵌套会导致双 store、权限与 MCP 状态分裂。测试里若需隔离,应 mount 独立 Provider 树而非嵌套。

建议学习步骤

  1. 阅读 store.ts createStore 完整实现
  2. 浏览 AppState 类型定义中的 teamContext / mcp / tasks 簇
  3. 阅读 getDefaultAppState 的 initialMode 与 teammate lazy require
  4. 阅读 AppStateProvider 的 bypass 修正与 useSettingsChange
  5. 阅读 useAppState 的 useSyncExternalStore 与 selector 注释
  6. 在 main.tsx 搜索 createStore 与 AppStateProvider

常见误区

注意

从 AppState.tsx import 类型会拉入 React——.ts 文件应 import AppStateStore.js

注意

getDefaultAppState 缺省不含 teamContext——main.tsx computeInitialTeamContext 补全

注意

setState 返回 prev 引用时 onChange 与 listeners 均不触发

注意

VoiceProvider 用 feature DCE——外部构建是 passthrough children

在架构中的位置

AppState 数据流:

getDefaultAppState() + main.tsx 补丁(teamContext、kairosEnabled…)
  → createStore(initial, onChangeAppState)
  → AppStateProvider 或 headless 裸 store
  → useAppState(s => s.field) / store.getState() 非 React 读取
  → setState(prev => ({ ...prev, field: x }))
  → onChangeAppState → settings / globalConfig / bootstrap override

REPL.tsx 是最大消费者:数十个 useAppState 切片避免整树重渲染。QueryEngine、Tool 执行通过 closure 里的 getState() 读最新 mcp.tools、toolPermissionContext。

createStore 实现要点

store.ts 刻意保持无依赖:

  • getState 闭包捕获 state 变量,不暴露 setter
  • setState 先 updater(prev),再 Object.is 比较;相等则完全 no-op(不调 onChange、不 notify)
  • subscribe 返回 unsubscribe 函数

这种语义与 Zustand / Redux 的「浅比较 bail-out」一致,但 AppState 体量大,依赖 Object.is 要求 updater 在不变时 return prev——teammateViewHelpers 中大量 if (!needsView) return prev 模式即为此。

onChange 回调可选,REPL 与 headless 均传入 onChangeAppState 做跨层同步。

源码引用: src/state/store.ts · 第 1–34 行(共 35 行)

   1| type Listener = () => void
   2| type OnChange<T> = (args: { newState: T; oldState: T }) => void
   3| 
   4| export type Store<T> = {
   5|   getState: () => T
   6|   setState: (updater: (prev: T) => T) => void
   7|   subscribe: (listener: Listener) => () => void
   8| }
   9| 
  10| export function createStore<T>(
  11|   initialState: T,
  12|   onChange?: OnChange<T>,
  13| ): Store<T> {
  14|   let state = initialState
  15|   const listeners = new Set<Listener>()
  16| 
  17|   return {
  18|     getState: () => state,
  19| 
  20|     setState: (updater: (prev: T) => T) => {
  21|       const prev = state
  22|       const next = updater(prev)
  23|       if (Object.is(next, prev)) return
  24|       state = next
  25|       onChange?.({ newState: next, oldState: prev })
  26|       for (const listener of listeners) listener()
  27|     },
  28| 
  29|     subscribe: (listener: Listener) => {
  30|       listeners.add(listener)
  31|       return () => listeners.delete(listener)
  32|     },
  33|   }
  34| }

AppState 类型:DeepImmutable 与例外

AppState 声明为 DeepImmutable&lt;{ ... }&gt; & { tasks; agentNameRegistry; ... }。

DeepImmutable 递归 readonly,防止组件意外 mutate nested 对象。但 tasks 含 TaskState 函数(abortController、onDone),agentNameRegistry 是 Map,sessionHooks 是 Map——这些被 union 排除在 DeepImmutable 外。

重要嵌套对象:

字段结构备注
toolPermissionContextToolPermissionContextmode、rules、bypass 标志
mcpclients/tools/commands/resourcespluginReconnectKey 仅作 effect 依赖
teamContextteammates 字典 + leadAgentIdswarm 多进程身份
speculationidle / active 判别联合active 含 abort、messagesRef
promptSuggestiontext + promptId + 时间戳与 PromptSuggestion 服务联动

CompletionBoundary 与 SpeculationState 也在此文件导出,供 compact / speculation 服务使用。

源码引用: src/state/AppStateStore.ts · 第 41–88 行(共 570 行)

  41| export type CompletionBoundary =
  42|   | { type: 'complete'; completedAt: number; outputTokens: number }
  43|   | { type: 'bash'; command: string; completedAt: number }
  44|   | { type: 'edit'; toolName: string; filePath: string; completedAt: number }
  45|   | {
  46|       type: 'denied_tool'
  47|       toolName: string
  48|       detail: string
  49|       completedAt: number
  50|     }
  51| 
  52| export type SpeculationResult = {
  53|   messages: Message[]
  54|   boundary: CompletionBoundary | null
  55|   timeSavedMs: number
  56| }
  57| 
  58| export type SpeculationState =
  59|   | { status: 'idle' }
  60|   | {
  61|       status: 'active'
  62|       id: string
  63|       abort: () => void
  64|       startTime: number
  65|       messagesRef: { current: Message[] } // Mutable ref - avoids array spreading per message
  66|       writtenPathsRef: { current: Set<string> } // Mutable ref - relative paths written to overlay
  67|       boundary: CompletionBoundary | null
  68|       suggestionLength: number
  69|       toolUseCount: number
  70|       isPipelined: boolean
  71|       contextRef: { current: REPLHookContext }
  72|       pipelinedSuggestion?: {
  73|         text: string
  74|         promptId: 'user_intent' | 'stated_intent'
  75|         generationRequestId: string | null
  76|       } | null
  77|     }
  78| 
  79| export const IDLE_SPECULATION_STATE: SpeculationState = { status: 'idle' }
  80| 
  81| export type FooterItem =
  82|   | 'tasks'
  83|   | 'tmux'
  84|   | 'bagel'
  85|   | 'teams'
  86|   | 'bridge'
  87|   | 'companion'
  88| 

源码引用: src/state/AppStateStore.ts · 第 89–158 行(共 570 行)

  89| export type AppState = DeepImmutable<{
  90|   settings: SettingsJson
  91|   verbose: boolean
  92|   mainLoopModel: ModelSetting
  93|   mainLoopModelForSession: ModelSetting
  94|   statusLineText: string | undefined
  95|   expandedView: 'none' | 'tasks' | 'teammates'
  96|   isBriefOnly: boolean
  97|   // Optional - only present when ENABLE_AGENT_SWARMS is true (for dead code elimination)
  98|   showTeammateMessagePreview?: boolean
  99|   selectedIPAgentIndex: number
 100|   // CoordinatorTaskPanel selection: -1 = pill, 0 = main, 1..N = agent rows.
 101|   // AppState (not local) so the panel can read it directly without prop-drilling
 102|   // through PromptInput → PromptInputFooter.
 103|   coordinatorTaskIndex: number
 104|   viewSelectionMode: 'none' | 'selecting-agent' | 'viewing-agent'
 105|   // Which footer pill is focused (arrow-key navigation below the prompt).
 106|   // Lives in AppState so pill components rendered outside PromptInput
 107|   // (CompanionSprite in REPL.tsx) can read their own focused state.
 108|   footerSelection: FooterItem | null
 109|   toolPermissionContext: ToolPermissionContext
 110|   spinnerTip?: string
 111|   // Agent name from --agent CLI flag or settings (for logo display)
 112|   agent: string | undefined
 113|   // Assistant mode fully enabled (settings + GrowthBook gate + trust).
 114|   // Single source of truth - computed once in main.tsx before option
 115|   // mutation, consumers read this instead of re-calling isAssistantMode().
 116|   kairosEnabled: boolean
 117|   // Remote session URL for --remote mode (shown in footer indicator)
 118|   remoteSessionUrl: string | undefined
 119|   // Remote session WS state (`claude assistant` viewer). 'connected' means the
 120|   // live event stream is open; 'reconnecting' = transient WS drop, backoff
 121|   // in progress; 'disconnected' = permanent close or reconnects exhausted.
 122|   remoteConnectionStatus:
 123|     | 'connecting'
 124|     | 'connected'
 125|     | 'reconnecting'
 126|     | 'disconnected'
 127|   // `claude assistant`: count of background tasks (Agent calls, teammates,
 128|   // workflows) running inside the REMOTE daemon child. Event-sourced from
 129|   // system/task_started and system/task_notification on the WS. The local
 130|   // AppState.tasks is always empty in viewer mode — the tasks live in a
 131|   // different process.
 132|   remoteBackgroundTaskCount: number
 133|   // Always-on bridge: desired state (controlled by /config or footer toggle)
 134|   replBridgeEnabled: boolean
 135|   // Always-on bridge: true when activated via /remote-control command, false when config-driven
 136|   replBridgeExplicit: boolean
 137|   // Outbound-only mode: forward events to CCR but reject inbound prompts/control
 138|   replBridgeOutboundOnly: boolean
 139|   // Always-on bridge: env registered + session created (= "Ready")
 140|   replBridgeConnected: boolean
 141|   // Always-on bridge: ingress WebSocket is open (= "Connected" - user on claude.ai)
 142|   replBridgeSessionActive: boolean
 143|   // Always-on bridge: poll loop is in error backoff (= "Reconnecting")
 144|   replBridgeReconnecting: boolean
 145|   // Always-on bridge: connect URL for Ready state (?bridge=envId)
 146|   replBridgeConnectUrl: string | undefined
 147|   // Always-on bridge: session URL on claude.ai (set when connected)
 148|   replBridgeSessionUrl: string | undefined
 149|   // Always-on bridge: IDs for debugging (shown in dialog when --verbose)
 150|   replBridgeEnvironmentId: string | undefined
 151|   replBridgeSessionId: string | undefined
 152|   // Always-on bridge: error message when connection fails (shown in BridgeDialog)
 153|   replBridgeError: string | undefined
 154|   // Always-on bridge: session name set via `/remote-control <name>` (used as session title)
 155|   replBridgeInitialName: string | undefined
 156|   // Always-on bridge: first-time remote dialog pending (set by /remote-control command)
 157|   showRemoteCallout: boolean
 158| }> & {

源码引用: src/state/AppStateStore.ts · 第 323–345 行(共 570 行)

 323|   teamContext?: {
 324|     teamName: string
 325|     teamFilePath: string
 326|     leadAgentId: string
 327|     // Self-identity for swarm members (separate processes in tmux panes)
 328|     // Note: This is different from toolUseContext.agentId which is for in-process subagents
 329|     selfAgentId?: string // Swarm member's own ID (same as leadAgentId for leaders)
 330|     selfAgentName?: string // Swarm member's name ('team-lead' for leaders)
 331|     isLeader?: boolean // True if this swarm member is the team leader
 332|     selfAgentColor?: string // Assigned color for UI (used by dynamically joined sessions)
 333|     teammates: {
 334|       [teammateId: string]: {
 335|         name: string
 336|         agentType?: string
 337|         color?: string
 338|         tmuxSessionName: string
 339|         tmuxPaneId: string
 340|         cwd: string
 341|         worktreePath?: string
 342|         spawnedAt: number
 343|       }
 344|     }
 345|   }

源码引用: src/state/AppStateStore.ts · 第 173–216 行(共 570 行)

 173|   mcp: {
 174|     clients: MCPServerConnection[]
 175|     tools: Tool[]
 176|     commands: Command[]
 177|     resources: Record<string, ServerResource[]>
 178|     /**
 179|      * Incremented by /reload-plugins to trigger MCP effects to re-run
 180|      * and pick up newly-enabled plugin MCP servers. Effects read this
 181|      * as a dependency; the value itself is not consumed.
 182|      */
 183|     pluginReconnectKey: number
 184|   }
 185|   plugins: {
 186|     enabled: LoadedPlugin[]
 187|     disabled: LoadedPlugin[]
 188|     commands: Command[]
 189|     /**
 190|      * Plugin system errors collected during loading and initialization.
 191|      * See {@link PluginError} type documentation for complete details on error
 192|      * structure, context fields, and display format.
 193|      */
 194|     errors: PluginError[]
 195|     // Installation status for background plugin/marketplace installation
 196|     installationStatus: {
 197|       marketplaces: Array<{
 198|         name: string
 199|         status: 'pending' | 'installing' | 'installed' | 'failed'
 200|         error?: string
 201|       }>
 202|       plugins: Array<{
 203|         id: string
 204|         name: string
 205|         status: 'pending' | 'installing' | 'installed' | 'failed'
 206|         error?: string
 207|       }>
 208|     }
 209|     /**
 210|      * Set to true when plugin state on disk has changed (background reconcile,
 211|      * /plugin menu install, external settings edit) and active components are
 212|      * stale. In interactive mode, user runs /reload-plugins to consume. In
 213|      * headless mode, refreshPluginState() auto-consumes via refreshActivePlugins().
 214|      */
 215|     needsRefresh: boolean
 216|   }

getDefaultAppState 默认值策略

getDefaultAppState 构造「空 REPL」快照:

  • settings: getInitialSettings() 合并 user/project/local/policy
  • mainLoopModel: null(表示默认模型,非显式 override)
  • toolPermissionContext.mode: teammate 且 plan_mode_required 时为 'plan',否则 'default'——lazy require ../utils/teammate.js 破循环依赖
  • mcp / plugins: 空数组,needsRefresh false
  • thinkingEnabled / promptSuggestionEnabled: 各自 shouldEnable* 函数
  • speculation: IDLE_SPECULATION_STATE
  • replBridge*: 全 false/undefined

未在默认值中出现的可选字段(teamContext、computerUseMcpState、tungsten*)由 main.tsx 或 feature gate 在 initialState 补丁。resume 路径从 transcript + sessionStorage 恢复 messages,AppState 仍从 getDefaultAppState 基底 merge。

源码引用: src/state/AppStateStore.ts · 第 456–503 行(共 570 行)

 456| export function getDefaultAppState(): AppState {
 457|   // Determine initial permission mode for teammates spawned with plan_mode_required
 458|   // Use lazy require to avoid circular dependency with teammate.ts
 459|   /* eslint-disable @typescript-eslint/no-require-imports */
 460|   const teammateUtils =
 461|     require('../utils/teammate.js') as typeof import('../utils/teammate.js')
 462|   /* eslint-enable @typescript-eslint/no-require-imports */
 463|   const initialMode: PermissionMode =
 464|     teammateUtils.isTeammate() && teammateUtils.isPlanModeRequired()
 465|       ? 'plan'
 466|       : 'default'
 467| 
 468|   return {
 469|     settings: getInitialSettings(),
 470|     tasks: {},
 471|     agentNameRegistry: new Map(),
 472|     verbose: false,
 473|     mainLoopModel: null, // alias, full name (as with --model or env var), or null (default)
 474|     mainLoopModelForSession: null,
 475|     statusLineText: undefined,
 476|     expandedView: 'none',
 477|     isBriefOnly: false,
 478|     showTeammateMessagePreview: false,
 479|     selectedIPAgentIndex: -1,
 480|     coordinatorTaskIndex: -1,
 481|     viewSelectionMode: 'none',
 482|     footerSelection: null,
 483|     kairosEnabled: false,
 484|     remoteSessionUrl: undefined,
 485|     remoteConnectionStatus: 'connecting',
 486|     remoteBackgroundTaskCount: 0,
 487|     replBridgeEnabled: false,
 488|     replBridgeExplicit: false,
 489|     replBridgeOutboundOnly: false,
 490|     replBridgeConnected: false,
 491|     replBridgeSessionActive: false,
 492|     replBridgeReconnecting: false,
 493|     replBridgeConnectUrl: undefined,
 494|     replBridgeSessionUrl: undefined,
 495|     replBridgeEnvironmentId: undefined,
 496|     replBridgeSessionId: undefined,
 497|     replBridgeError: undefined,
 498|     replBridgeInitialName: undefined,
 499|     showRemoteCallout: false,
 500|     toolPermissionContext: {
 501|       ...getEmptyToolPermissionContext(),
 502|       mode: initialMode,
 503|     },

源码引用: src/state/AppStateStore.ts · 第 512–568 行(共 570 行)

 512|     mcp: {
 513|       clients: [],
 514|       tools: [],
 515|       commands: [],
 516|       resources: {},
 517|       pluginReconnectKey: 0,
 518|     },
 519|     plugins: {
 520|       enabled: [],
 521|       disabled: [],
 522|       commands: [],
 523|       errors: [],
 524|       installationStatus: {
 525|         marketplaces: [],
 526|         plugins: [],
 527|       },
 528|       needsRefresh: false,
 529|     },
 530|     todos: {},
 531|     remoteAgentTaskSuggestions: [],
 532|     notifications: {
 533|       current: null,
 534|       queue: [],
 535|     },
 536|     elicitation: {
 537|       queue: [],
 538|     },
 539|     thinkingEnabled: shouldEnableThinkingByDefault(),
 540|     promptSuggestionEnabled: shouldEnablePromptSuggestion(),
 541|     sessionHooks: new Map(),
 542|     inbox: {
 543|       messages: [],
 544|     },
 545|     workerSandboxPermissions: {
 546|       queue: [],
 547|       selectedIndex: 0,
 548|     },
 549|     pendingWorkerRequest: null,
 550|     pendingSandboxRequest: null,
 551|     promptSuggestion: {
 552|       text: null,
 553|       promptId: null,
 554|       shownAt: 0,
 555|       acceptedAt: 0,
 556|       generationRequestId: null,
 557|     },
 558|     speculation: IDLE_SPECULATION_STATE,
 559|     speculationSessionTimeSavedMs: 0,
 560|     skillImprovement: {
 561|       suggestion: null,
 562|     },
 563|     authVersion: 0,
 564|     initialMessage: null,
 565|     effortValue: undefined,
 566|     activeOverlays: new Set<string>(),
 567|     fastMode: false,
 568|   }

AppStateProvider 与 React Hooks

AppStateProvider 职责:

  1. 禁止嵌套 — hasAppStateContext 检测
  2. 稳定 store — useState(() => createStore(...)) 只运行一次
  3. Mount 修正 bypass — 若 remote settings 在 Provider 挂载前禁用 bypass,useEffect 一次性 setState 为 createDisabledBypassPermissionsContext
  4. settings 文件监听 — useSettingsChange + applySettingsChange 把磁盘变更写入 AppState
  5. 上下文包装 — MailboxProvider、VoiceProvider(feature gated)

useAppState(selector) 用 useSyncExternalStore(store.subscribe, get, get)——SSR 与 client 同一 get 快照。

useSetAppState 只取 store.setState,不订阅,适合纯写入组件。

useAppStateStore 暴露完整 store 给需要 getState 的子树。

useAppStateMaybeOutsideOfProvider 在无 Provider 时返回 undefined 而非 throw——Doctor 等可选挂载场景。

源码引用: src/state/AppState.tsx · 第 37–109 行(共 201 行)

  37| export {
  38|   type AppState,
  39|   type AppStateStore,
  40|   type CompletionBoundary,
  41|   getDefaultAppState,
  42|   IDLE_SPECULATION_STATE,
  43|   type SpeculationResult,
  44|   type SpeculationState,
  45| } from './AppStateStore.js'
  46| 
  47| export const AppStoreContext = React.createContext<AppStateStore | null>(null)
  48| 
  49| type Props = {
  50|   children: React.ReactNode
  51|   initialState?: AppState
  52|   onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void
  53| }
  54| 
  55| const HasAppStateContext = React.createContext<boolean>(false)
  56| 
  57| export function AppStateProvider({
  58|   children,
  59|   initialState,
  60|   onChangeAppState,
  61| }: Props): React.ReactNode {
  62|   // Don't allow nested AppStateProviders.
  63|   const hasAppStateContext = useContext(HasAppStateContext)
  64|   if (hasAppStateContext) {
  65|     throw new Error(
  66|       'AppStateProvider can not be nested within another AppStateProvider',
  67|     )
  68|   }
  69| 
  70|   // Store is created once and never changes -- stable context value means
  71|   // the provider never triggers re-renders. Consumers subscribe to slices
  72|   // via useSyncExternalStore in useAppState(selector).
  73|   const [store] = useState(() =>
  74|     createStore<AppState>(
  75|       initialState ?? getDefaultAppState(),
  76|       onChangeAppState,
  77|     ),
  78|   )
  79| 
  80|   // Check on mount if bypass mode should be disabled
  81|   // This handles the race condition where remote settings load BEFORE this component mounts,
  82|   // meaning the settings change notification was sent when no listeners were subscribed.
  83|   // On subsequent sessions, the cached remote-settings.json is read during initial setup,
  84|   // but on the first session the remote fetch may complete before React mounts.
  85|   useEffect(() => {
  86|     const { toolPermissionContext } = store.getState()
  87|     if (
  88|       toolPermissionContext.isBypassPermissionsModeAvailable &&
  89|       isBypassPermissionsModeDisabled()
  90|     ) {
  91|       logForDebugging(
  92|         'Disabling bypass permissions mode on mount (remote settings loaded before mount)',
  93|       )
  94|       store.setState(prev => ({
  95|         ...prev,
  96|         toolPermissionContext: createDisabledBypassPermissionsContext(
  97|           prev.toolPermissionContext,
  98|         ),
  99|       }))
 100|     }
 101|     // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
 102|   }, [])
 103| 
 104|   // Listen for external settings changes and sync to AppState.
 105|   // This ensures file watcher changes propagate through the app --
 106|   // shared with the headless/SDK path via applySettingsChange.
 107|   const onSettingsChange = useEffectEvent((source: SettingSource) =>
 108|     applySettingsChange(source, store.setState),
 109|   )

源码引用: src/state/AppState.tsx · 第 126–163 行(共 201 行)

 126|   if (!store) {
 127|     throw new ReferenceError(
 128|       'useAppState/useSetAppState cannot be called outside of an <AppStateProvider />',
 129|     )
 130|   }
 131|   return store
 132| }
 133| 
 134| /**
 135|  * Subscribe to a slice of AppState. Only re-renders when the selected value
 136|  * changes (compared via Object.is).
 137|  *
 138|  * For multiple independent fields, call the hook multiple times:
 139|  * ```
 140|  * const verbose = useAppState(s =&gt; s.verbose)
 141|  * const model = useAppState(s =&gt; s.mainLoopModel)
 142|  * ```
 143|  *
 144|  * Do NOT return new objects from the selector -- Object.is will always see
 145|  * them as changed. Instead, select an existing sub-object reference:
 146|  * ```
 147|  * const { text, promptId } = useAppState(s =&gt; s.promptSuggestion) // good
 148|  * ```
 149|  */
 150| export function useAppState<T>(selector: (state: AppState) => T): T {
 151|   const store = useAppStore()
 152| 
 153|   const get = () => {
 154|     const state = store.getState()
 155|     const selected = selector(state)
 156| 
 157|     if ("external" === 'ant' && state === selected) {
 158|       throw new Error(
 159|         `Your selector in \`useAppState(${selector.toString()})\` returned the original state, which is not allowed. You must instead return a property for optimised rendering.`,
 160|       )
 161|     }
 162| 
 163|     return selected

源码引用: src/state/AppState.tsx · 第 170–199 行(共 201 行)

 170|  * Get the setAppState updater without subscribing to any state.
 171|  * Returns a stable reference that never changes -- components using only
 172|  * this hook will never re-render from state changes.
 173|  */
 174| export function useSetAppState(): (
 175|   updater: (prev: AppState) => AppState,
 176| ) => void {
 177|   return useAppStore().setState
 178| }
 179| 
 180| /**
 181|  * Get the store directly (for passing getState/setState to non-React code).
 182|  */
 183| export function useAppStateStore(): AppStateStore {
 184|   return useAppStore()
 185| }
 186| 
 187| const NOOP_SUBSCRIBE = () => () => {}
 188| 
 189| /**
 190|  * Safe version of useAppState that returns undefined if called outside of AppStateProvider.
 191|  * Useful for components that may be rendered in contexts where AppStateProvider isn't available.
 192|  */
 193| export function useAppStateMaybeOutsideOfProvider<T>(
 194|   selector: (state: AppState) => T,
 195| ): T | undefined {
 196|   const store = useContext(AppStoreContext)
 197|   return useSyncExternalStore(store ? store.subscribe : NOOP_SUBSCRIBE, () =>
 198|     store ? selector(store.getState()) : undefined,
 199|   )

headless 与 REPL 初始状态差异

main.tsx 在渲染 REPL 前计算 initialState 补丁:

  • kairosEnabled、agent(--agent)、teamContext(computeInitialTeamContext)
  • mcp 预连接结果、plugins 列表
  • initialMessage(CLI -p / plan exit)
  • remote / bridge 标志位

Headless 路径(print.ts、SDK):

headlessInitialState = { ...getDefaultAppState(), ...patches }
headlessStore = createStore(headlessInitialState, onChangeAppState)

无 Provider,但 onChangeAppState 仍同步 model 到 settings 与 bootstrap。调试 headless 与 interactive 行为差异时,对比 initialState 补丁而非 getDefaultAppState 本身。

源码引用: src/main.tsx · 第 2650–2655 行(共 6604 行)

2650|         // that need MCP pass --mcp-config explicitly.
2651|         !isBareMode()
2652|           ? fetchClaudeAIMcpConfigsIfEligible().then(configs => {
2653|               const { allowed, blocked } = filterMcpServersByPolicy(configs)
2654|               if (blocked.length > 0) {
2655|                 process.stderr.write(

迁移注释与 import hygiene

AppState.tsx 顶部 TODO:类型 re-export 仅为 back-compat,新代码应 import type { AppState } from './AppStateStore.js' 避免 .ts 文件依赖 React compiler runtime。

React Compiler(_c memo cache)已应用于 Provider 与 hooks——阅读源码时注意 $[n] 槽位是编译器产物,非手写 useMemo。

export 从 AppState.tsx:AppState、AppStateStore、CompletionBoundary、SpeculationState、IDLE_SPECULATION_STATE、getDefaultAppState。工具链 grep 时应同时搜 AppStateStore.ts 与 AppState.tsx。

本章小结与延伸

app-state-core = 订阅存储 + 类型定义 + React 绑定。下一章 app-state-selectors,读纯函数 selector 与 onChangeAppState 副作用。 继续学习:

  • app-state-selectors
  • state-boundaries
Prev
模块: state
Next
app-state-selectors · selectors 与 onChangeAppState