Appearance
03 — 状态管理 — State
概述
Claude Code 的状态管理有意保持简单——不使用 Redux、Zustand 或任何第三方状态库,而是自研了一个约 35 行代码的 Pub/Sub Store。这个选择反映了项目的设计哲学:状态流向是单一的(Tool 执行 → 写入 Store → UI 订阅),不需要复杂的中间件。
理解状态管理的关键在于理解两个概念:
AppState— 是什么:全局状态的类型定义,描述了"应用此刻的全部信息"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 是整个应用的"全貌快照"。以下按功能分组列出核心字段:
对话状态
| 字段 | 类型 | 说明 |
|---|---|---|
messages | Message[] | 对话消息列表(用户消息 + 助手消息 + 工具结果 + 系统消息) |
inProgressToolUseIDs | Set<string> | 当前正在执行的工具调用 ID(用于 UI 显示进度) |
speculationState | SpeculationState | 推测执行状态(预测用户下一步操作并提前执行) |
模型与配置
| 字段 | 类型 | 说明 |
|---|---|---|
mainLoopModel | ModelSetting | 当前使用的模型 |
thinkingEnabled | boolean | 是否启用思考模式 |
effortSetting | EffortValue | 推理努力级别 (low/medium/high) |
settings | SettingsJson | 完整配置(来自多层合并) |
权限
| 字段 | 类型 | 说明 |
|---|---|---|
toolPermissionContext | ToolPermissionContext | 权限模式 + 规则集(allow/deny/ask) |
denialTrackingState | DenialTrackingState | 拒绝记录(避免反复询问同一操作) |
工具与扩展
| 字段 | 类型 | 说明 |
|---|---|---|
tools | Tools | 所有可用工具(内置 + MCP + Plugin) |
commands | Command[] | 所有斜杠命令 |
mcpClients | MCPServerConnection[] | MCP 服务器连接 |
mcpResources | Record<string, ServerResource[]> | MCP 资源 |
agentDefinitions | AgentDefinitionsResult | Agent 定义(内置 + 自定义) |
loadedPlugins | LoadedPlugin[] | 已加载插件 |
任务与 Agent
| 字段 | 类型 | 说明 |
|---|---|---|
tasks | { [taskId]: TaskState } | 后台任务映射(可变,不受 DeepImmutable) |
agentNameRegistry | Map<string, AgentId> | Agent 名称 → ID 映射 |
foregroundedTaskId | string? | 当前前台任务 |
UI 状态
| 字段 | 类型 | 说明 |
|---|---|---|
notifications | Notification[] | 通知列表 |
todoList | TodoList | 待办列表 |
sessionHooks | SessionHooksState | 会话钩子状态 |
promptSuggestionEnabled | boolean | 是否启用 Prompt 建议 |
状态流向
状态永远是 单向流动 的:写入方通过 setAppState(prev => next) 更新状态,读取方通过 subscribe 或 getState 获取状态。不存在双向绑定。
ToolUseContext — 工具执行的完整上下文
ToolUseContext(定义在 Tool.ts)是每次 Tool 调用时传入的上下文对象。它是 Tool 与外部世界交互的唯一接口——Tool 不直接 import AppState,而是通过 context 的回调间接操作。
关键字段说明
| 字段 | 说明 | 为什么需要 |
|---|---|---|
getAppState() / setAppState() | 读写全局状态 | Tool 需要读取权限规则、写入任务状态等 |
options | 只读配置(模型、工具列表、命令列表、MCP 客户端等) | Tool 需要知道当前环境 |
abortController | 中断信号 | 用户按 Ctrl+C 时级联中止所有进行中的工具 |
readFileState | 文件内容 LRU 缓存 | 避免重复读取相同文件 |
setAppStateForTasks | 始终有效的 setState(不被子代理隔离) | 后台任务/会话钩子需要修改 root 状态 |
handleElicitation | URL 认证弹窗回调 | 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 避免在组件中写重复的状态查询逻辑。