全局状态:AppStateStore.ts 的 Zustand 式设计
管理一个复杂 CLI 工具的全局状态是一项挑战。Claude Code 需要在整个生命周期内追踪消息历史、工具权限、MCP 连接、后台任务、Todo 列表、推测执行状态……数十个相互关联的数据片段。
本文深入解析 AppStateStore.ts(454 行)和配套的 store.ts(34 行),理解 Claude Code 如何设计它的状态管理系统。
为什么不用 Redux 或 Context?
Redux 的问题
Redux 引入了大量样板代码:actions、reducers、selectors、middleware……对于一个 CLI 工具来说过于重量级,且 Redux 的调试工具是为浏览器设计的。
React Context 的问题
Context 的痛点在于性能:任何 context 值的变化都会导致所有消费者重渲染,即便消费者只关心 context 中的某一个字段。对于像 Claude Code 这样频繁更新流式消息的应用,这会导致大量不必要的重渲染。
Claude Code 的选择:自定义 Store
Claude Code 实现了一个极简的自定义 Store,设计思路与 Zustand 几乎完全一致——但不依赖外部库,代码总共只有 34 行(source/src/state/store.ts):
// source/src/state/store.ts
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void
export type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
export function createStore<T>(
initialState: T,
onChange?: OnChange<T>,
): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater: (prev: T) => T) => {
const prev = state
const next = updater(prev)
if (Object.is(next, prev)) return // 相同引用则不触发更新
state = next
onChange?.({ newState: next, oldState: prev })
for (const listener of listeners) listener()
},
subscribe: (listener: Listener) => {
listeners.add(listener)
return () => listeners.delete(listener) // 返回取消订阅函数
},
}
}
这个设计的精妙之处:
Object.is比较:如果setState返回的新状态与旧状态是同一个引用,则直接跳过,不触发任何监听器- 函数式更新:
setState接收updater: (prev: T) => T而非直接接收新状态,确保更新基于最新状态 onChange回调:提供了一个逃生舱,供非 React 代码(如 daemon 进程通信)监听状态变化- 订阅模式:返回取消订阅函数,与
useSyncExternalStore完美配合
AppState 的完整数据结构
AppState 类型定义在 source/src/state/AppStateStore.ts 中,是一个庞大的对象。我们按功能分组理解它:
基础配置层
export type AppState = DeepImmutable<{
// 用户设置(从 ~/.claude/settings.json 加载)
settings: SettingsJson
// 是否启用详细日志
verbose: boolean
// 当前使用的模型(可被 --model 参数覆盖)
mainLoopModel: ModelSetting
mainLoopModelForSession: ModelSetting
// 底部状态栏文字
statusLineText: string | undefined
// ...
}>
权限与模式层
{
// 工具权限上下文:包含当前 PermissionMode 和所有 allow/deny 规则
toolPermissionContext: ToolPermissionContext
// 是否启用 bypassPermissions 模式(需要特殊条件)
// PermissionMode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk'
}
toolPermissionContext 是权限系统的核心载体,它包含:
mode:当前权限模式allowRules/denyRules:会话级别的规则列表isBypassPermissionsModeAvailable:是否允许进入 bypass 模式
任务与代理层
{
// 所有后台任务的状态映射(TaskId -> TaskState)
// 不在 DeepImmutable 内,因为 TaskState 包含函数类型
tasks: { [taskId: string]: TaskState }
// Agent 名称注册表(name -> AgentId)
// 最新注册胜出,用于 SendMessage 按名称路由
agentNameRegistry: Map<string, AgentId>
// 被前台显示的任务 ID
foregroundedTaskId?: string
}
MCP 连接层
{
mcp: {
// 所有活跃的 MCP 服务器连接
clients: MCPServerConnection[]
// 从 MCP 服务器动态加载的工具
tools: Tool[]
// 从 MCP 服务器动态加载的命令
commands: Command[]
// MCP 服务器提供的资源
resources: Record<string, ServerResource[]>
// 重载触发器:/reload-plugins 时递增,触发 useEffect 重新连接
pluginReconnectKey: number
}
}
推测执行层(Speculation)
这是一个高级特性,用于在用户还没提交下一个指令时,提前预测并执行可能的工具调用:
export type SpeculationState =
| { status: 'idle' }
| {
status: 'active'
id: string
abort: () => void
startTime: number
// 使用 Ref 而非数组展开,避免每条消息都创建新数组
messagesRef: { current: Message[] }
writtenPathsRef: { current: Set<string> }
boundary: CompletionBoundary | null
suggestionLength: number
toolUseCount: number
isPipelined: boolean
// ...
}
// AppState 中:
{
speculation: SpeculationState
speculationSessionTimeSavedMs: number // 累计节省的时间
}
Bridge(远程控制)层
Bridge 是 Claude Code 与 claude.ai 网页端连接的机制:
{
replBridgeEnabled: boolean // 是否启用 bridge
replBridgeConnected: boolean // 是否已建立连接("Ready"状态)
replBridgeSessionActive: boolean // WebSocket 是否活跃("Connected"状态)
replBridgeReconnecting: boolean // 是否在重连中
replBridgeConnectUrl: string | undefined // 连接 URL
replBridgeSessionUrl: string | undefined // claude.ai 会话 URL
replBridgeError: string | undefined // 连接错误信息
}
通知与提示层
{
notifications: {
current: Notification | null
queue: Notification[]
}
// MCP Elicitation(MCP 2.0 新特性:服务器向用户请求信息)
elicitation: {
queue: ElicitationRequestEvent[]
}
// 提示建议(预测用户下一步输入)
promptSuggestion: {
text: string | null
promptId: 'user_intent' | 'stated_intent' | null
shownAt: number
acceptedAt: number
generationRequestId: string | null
}
}
Teams(多 Agent 协作)层
{
teamContext?: {
teamName: string
teamFilePath: string
leadAgentId: string
selfAgentId?: string // 本 Agent 自身 ID
selfAgentName?: string // 本 Agent 名称
isLeader?: boolean // 是否是 team leader
teammates: {
[teammateId: string]: {
name: string
color?: string
tmuxSessionName: string
tmuxPaneId: string
cwd: string
// ...
}
}
}
}
DeepImmutable 的设计意图
注意 AppState 的顶层类型包装:
export type AppState = DeepImmutable<{
// 大部分字段...
}> & {
// 包含函数类型的字段(不能 DeepImmutable)
tasks: { [taskId: string]: TaskState }
agentNameRegistry: Map<string, AgentId>
}
DeepImmutable 工具类型将所有嵌套字段标记为 readonly,在编译期阻止意外的状态直接修改。所有状态变更必须通过 setState 传入更新函数,返回新的不可变对象。
但 tasks 和 agentNameRegistry 被排除在外——因为 TaskState 包含函数类型(如 abort 回调),而 Map 本身是可变的,无法被 DeepImmutable 正确处理。
getDefaultAppState:初始状态工厂
export function getDefaultAppState(): AppState {
// 使用 lazy require 避免与 teammate.ts 的循环依赖
const teammateUtils = require('../utils/teammate.js')
// 检测是否在 "plan 模式" 下作为 teammate 启动
const initialMode: PermissionMode =
teammateUtils.isTeammate() && teammateUtils.isPlanModeRequired()
? 'plan'
: 'default'
return {
settings: getInitialSettings(),
tasks: {},
agentNameRegistry: new Map(),
verbose: false,
toolPermissionContext: {
...getEmptyToolPermissionContext(),
mode: initialMode,
},
// ... 其他字段的初始值
}
}
这个函数在 AppStateProvider 的 useState 初始化时被调用一次,此后状态存活于 React 树之外的 Store 实例中。
小结
Claude Code 的状态管理方案极简而有效:34 行自定义 Store + 精心设计的 AppState 数据结构。关键设计决策:
- 不引入 Redux/Zustand 等外部库,自制等效实现,减少依赖
DeepImmutable,在编译期强制不可变性- 函数式更新,避免过时的 state 读取
Object.is短路,相同引用不触发重渲染onChange逃生舱,供非 React 代码监听状态
下一篇文章将聚焦 useAppState 和 useSetAppState 两个 hooks,理解 React 组件如何高效订阅这个 Store。