本章总览
src/state/ 是 Claude Code 的「React 可订阅全局状态层」:用轻量 createStore 实现 Zustand 风格的 getState / setState / subscribe,AppStateStore.ts 定义约 450 行巨型 AppState 类型与默认值,AppState.tsx 提供 Provider 与 useAppState 切片订阅(6 个文件,约 1196 行)。务必区分:bootstrap/state.ts(进程级 session/cost 全局单例)≠ state/ 目录(Ink REPL 的 React 状态树)。
总览图
学完本章你应该能
- 建立 AppState 与 bootstrap/state 双轨心智模型
- 理解 useAppState 切片订阅与 Object.is 相等性约束
- 能从 REPL 反查 viewingAgentTaskId、teamContext 等字段来源
- 知道 onChangeAppState 如何把 UI 变更同步到磁盘与 CCR
建议学习步骤
- 浏览下方子章节导航表,选择入口主题
- 点击 SourceTree 中的 state/ 文件跳转到对应讲解
- 建议顺序:app-state-core → app-state-selectors → teammate-state → state-boundaries
模块在架构中的位置
state 是 src/ 下的一级目录,共 6 个文件、1,196 行。建议结合「系统架构」章节理解它与其他层的调用关系。
模块 UML 图表
状态更新路径
概览
| 指标 | 数值 |
|---|---|
| 行数 | 1,196 |
| 文件 | 6 |
子章节导航
| 子章节 | 主题 | 核心路径 |
|---|---|---|
| app-state-core | store、AppState 类型、Provider | store.ts、AppStateStore.ts、AppState.tsx |
| app-state-selectors | 纯函数 selector、副作用同步 | selectors.ts、onChangeAppState.ts |
| teammate-state | 队友 transcript 视图、swarm | teammateViewHelpers.ts、useSwarmInitialization.ts |
| state-boundaries | bootstrap、持久化、文件缓存 | bootstrap/state.ts、sessionStorage.ts、fileStateCache.ts |
双轨状态:AppState vs bootstrap/state
Claude Code 刻意把状态拆成两层,避免 React 树之外的业务代码 import Ink:
| 层 | 位置 | 生命周期 | 典型字段 |
|---|---|---|---|
| AppState | state/AppStateStore.ts | 随 AppStateProvider 存在;headless 用 createStore 无 Provider | mainLoopModel、tasks、mcp.clients、viewingAgentTaskId |
| bootstrap STATE | bootstrap/state.ts | 进程级单例;注释「DO NOT ADD MORE STATE HERE」 | sessionId、totalCostUSD、mainLoopModelOverride、cwd |
onChangeAppState 是两条轨道的接缝之一:当 mainLoopModel 在 AppState 中变化时,同步调用 setMainLoopModelOverride 写入 bootstrap,并 updateSettingsForSource 落盘。
非 React 代码(query 循环、Tool 执行、sessionStorage)应读 bootstrap 或接收 getState() 快照,而不是 import useAppState。
store 与订阅模型
store.ts 的 createStore 实现极简:
setState(updater):
prev = state
next = updater(prev)
if Object.is(next, prev) return // 无变化则静默
state = next
onChange?.({ newState, oldState })
notify all listeners
AppStateProvider 用 useState(() => createStore(...)) 保证 store 实例稳定——Provider 自身不因 setState 重渲染;消费者通过 useSyncExternalStore 订阅。
useAppState(selector) 要求 selector 返回已有子对象引用或原始值,禁止 s => ({ ...s.foo }) 这种每次新建对象的选择器,否则 Object.is 永远为 false 导致无限重渲染。
AppState 字段簇概览
AppState 不是扁平 KV,而是按产品域分簇(节选):
| 簇 | 代表字段 | 消费方 |
|---|---|---|
| 模型与模式 | mainLoopModel、toolPermissionContext、fastMode | /model、Shift+Tab、query config |
| 任务与队友 | tasks、viewingAgentTaskId、teamContext | BackgroundTasksDialog、SendMessage |
| MCP / 插件 | mcp.*、plugins.* | useManageMCPConnections、/reload-plugins |
| Bridge / Remote | replBridge*、remoteConnectionStatus | replBridge、/remote-control |
| UI 壳层 | expandedView、footerSelection、activeOverlays | PromptInputFooter、Escape 协调 |
tasks 与 agentNameRegistry 被 DeepImmutable 排除函数类型;Map / Set 字段(fileHistory.trackedFiles、activeOverlays)在 setState 时需替换引用以触发订阅。
目录树展示 src/state/ 全部 6 个文件。相关边界文件(bootstrap/state.ts、utils/sessionStorage.ts)在 state-boundaries 子章节通过 highlightFiles 与高亮锚点跳转。
与其他模块的边界
| 协作方 | 关系 |
|---|---|
| screens/REPL.tsx | 包裹 AppStateProvider;大量 useAppState 切片 |
| main.tsx | headless createStore(initialState, onChangeAppState) |
| hooks/useSwarmInitialization.ts | resume 时写入 teamContext |
| hooks/useTeammateViewAutoExit.ts | 监听 viewed task status,调用 exitTeammateView |
| utils/sessionStorage.ts | 持久化 transcript;与 bootstrap sessionId 对齐 |
| utils/fileStateCache.ts | Tool 读文件缓存;compact 时 clone/merge |
| services/mcp/* | 连接状态写入 appState.mcp |
调试「状态不同步」:先确认改的是 AppState 还是 bootstrap;再查 onChangeAppState 是否覆盖该字段;最后看 setState 是否返回同一引用导致订阅未触发。
本章小结与延伸
state 模块 = REPL 与 Ink 组件的单一可订阅真相源。先懂 store + AppState 形状,再读 selectors 与 teammate 视图切换,最后理解 bootstrap / sessionStorage / FileStateCache 三条边界。 继续学习: