Skip to content

03 — 状态管理 — State

概述

Claude Code 的状态管理有意保持简单——不使用 Redux、Zustand 或任何第三方状态库,而是自研了一个约 35 行代码的 Pub/Sub Store。这个选择反映了项目的设计哲学:状态流向是单一的(Tool 执行 → 写入 Store → UI 订阅),不需要复杂的中间件。

理解状态管理的关键在于理解两个概念:

  1. AppState — 是什么:全局状态的类型定义,描述了"应用此刻的全部信息"
  2. ToolUseContext — 怎么用:每次工具调用时的上下文对象,包含读写 AppState 的回调

Store 实现 (state/store.ts)

typescript
type Store<T> = {
  getState: () => T                              // 读取当前状态
  setState: (updater: (prev: T) => T) => void    // 不可变更新
  subscribe: (listener: Listener) => () => void  // 订阅变更
}

核心实现约 35 行。关键行为:

  • 不可变语义setState 接收 prev => next 函数,用 Object.is() 判断是否真的变了,避免无意义的通知
  • 同步通知setState 调用后立即通知所有订阅者(包括 React 组件的重渲染)
  • 可选 onChange 回调:通过 onChangeAppState.ts 监听状态变更,用于持久化和遥测

AppState 类型 (state/AppStateStore.ts)

AppState 是整个应用的"全貌快照"。以下按功能分组列出核心字段:

对话状态

字段类型说明
messagesMessage[]对话消息列表(用户消息 + 助手消息 + 工具结果 + 系统消息)
inProgressToolUseIDsSet<string>当前正在执行的工具调用 ID(用于 UI 显示进度)
speculationStateSpeculationState推测执行状态(预测用户下一步操作并提前执行)

模型与配置

字段类型说明
mainLoopModelModelSetting当前使用的模型
thinkingEnabledboolean是否启用思考模式
effortSettingEffortValue推理努力级别 (low/medium/high)
settingsSettingsJson完整配置(来自多层合并)

权限

字段类型说明
toolPermissionContextToolPermissionContext权限模式 + 规则集(allow/deny/ask)
denialTrackingStateDenialTrackingState拒绝记录(避免反复询问同一操作)

工具与扩展

字段类型说明
toolsTools所有可用工具(内置 + MCP + Plugin)
commandsCommand[]所有斜杠命令
mcpClientsMCPServerConnection[]MCP 服务器连接
mcpResourcesRecord<string, ServerResource[]>MCP 资源
agentDefinitionsAgentDefinitionsResultAgent 定义(内置 + 自定义)
loadedPluginsLoadedPlugin[]已加载插件

任务与 Agent

字段类型说明
tasks{ [taskId]: TaskState }后台任务映射(可变,不受 DeepImmutable)
agentNameRegistryMap<string, AgentId>Agent 名称 → ID 映射
foregroundedTaskIdstring?当前前台任务

UI 状态

字段类型说明
notificationsNotification[]通知列表
todoListTodoList待办列表
sessionHooksSessionHooksState会话钩子状态
promptSuggestionEnabledboolean是否启用 Prompt 建议

状态流向

状态永远是 单向流动 的:写入方通过 setAppState(prev => next) 更新状态,读取方通过 subscribegetState 获取状态。不存在双向绑定。

ToolUseContext — 工具执行的完整上下文

ToolUseContext(定义在 Tool.ts)是每次 Tool 调用时传入的上下文对象。它是 Tool 与外部世界交互的唯一接口——Tool 不直接 import AppState,而是通过 context 的回调间接操作。

关键字段说明

字段说明为什么需要
getAppState() / setAppState()读写全局状态Tool 需要读取权限规则、写入任务状态等
options只读配置(模型、工具列表、命令列表、MCP 客户端等)Tool 需要知道当前环境
abortController中断信号用户按 Ctrl+C 时级联中止所有进行中的工具
readFileState文件内容 LRU 缓存避免重复读取相同文件
setAppStateForTasks始终有效的 setState(不被子代理隔离)后台任务/会话钩子需要修改 root 状态
handleElicitationURL 认证弹窗回调MCP 服务器要求 OAuth 认证时使用
requireCanUseTool是否强制走权限检查推测执行模式下用于 overlay 路径重写

子代理状态隔离

AgentTool 创建子代理时,forkSubagent.ts 会创建一个隔离的 ToolUseContext

关键设计

  • 子代理的 setAppState() 是 no-op——子代理的状态修改不会影响主会话的 UI
  • setAppStateForTasks 会穿透隔离,因为后台任务(如 Task 注册/清理)需要在 root store 中全局可见
  • 文件历史、归属追踪、Todo 通过 agentId 天然隔离
  • readFileState(LRU 缓存)是独立实例,子代理读取的文件不会挤占主会话的缓存

onChangeAppState.ts — 状态变更监听

这个模块监听 AppState 的变更,触发副作用:

  • 持久化权限规则:当用户批准/拒绝工具权限时,规则被写入磁盘(~/.claude/settings.json),下次会话自动恢复
  • 遥测事件:权限决策、模型切换等操作会被记录到 analytics
  • 通知调度:状态变更可能触发 UI 通知(如"已切换到 Opus 模型")

state/selectors.ts — 派生状态

Selectors 是纯函数,从 AppState 派生计算值。例如:

  • 当前是否有正在执行的工具?→ inProgressToolUseIDs.size > 0
  • 当前权限模式是什么?→ toolPermissionContext.mode
  • Teammate 视图相关的状态 → teammateViewHelpers.ts

Selectors 避免在组件中写重复的状态查询逻辑。