Book 第三十四章:AppState 为什么是 UI 与 runtime 的接缝层
第六部分:迁移与附录

第三十四章:AppState 为什么是 UI 与 runtime 的接缝层

把 AppState 当成宿主边界状态容器来读,拆 UI 投影、runtime 注册表和副作用桥。

1. 为什么这章必须单独拆

前面的文档已经反复提到 AppState,但一直没有把它当成一个正式的边界来讲。

这会带来一个很大的误判:

  • 很容易把它看成普通 React UI state
  • 也很容易反过来,把它当成“所有状态都往里塞”的大对象

而从源码看,AppState 其实处在 Claude Code 最尴尬、也最值钱的一条边界线上:

  • 一头连着 REPL、Ink、各种面板和交互投影
  • 一头连着 task、agent、MCP、plugin、remote bridge、权限模式这些 runtime 子系统
  • 中间还夹着 settings 持久化、session metadata 同步、环境变量重载这些副作用

如果不把这条线拆清楚,后面做 C# 版时最容易抄出一个巨大的 God Object。

相关源码锚点:

  • src/state/AppStateStore.ts
  • src/state/AppState.tsx
  • src/state/onChangeAppState.ts
  • src/state/selectors.ts
  • src/screens/REPL.tsx
  • src/components/App.tsx

2. 先说结论

我对这一块的判断是:

Claude Code 里的 AppState 不是纯 UI state,也不是纯 session core state,而是一个“宿主边界状态容器”。

它大致同时装了四类东西:

  1. 会话核心配置 比如 settingsmainLoopModeltoolPermissionContextthinkingEnabled
  2. 运行时注册表 比如 tasksagentNameRegistrymcpplugins
  3. 宿主投影状态 比如 expandedViewfooterSelectionviewSelectionMode
  4. 临时工作流状态 比如 promptSuggestionspeculationactiveOverlayspendingPlanVerification

这里最关键的设计点有三个:

  • AppStateStore 更像运行时 store,不像 React component state。
  • onChangeAppState() 是真正的边界桥,不只是 setState 回调。
  • 当前实现是刻意混合层,值钱的地方不在“照抄”,而在“拆分准则”。

3. 总体结构图

flowchart TD
    A["Query / Task / MCP / Plugin / Remote 子系统"] --> B["AppStateStore"]
    C["REPL / 组件 / 面板 / 状态条"] --> B

    B --> D["selector + useSyncExternalStore"]
    D --> C

    B --> E["onChangeAppState"]
    E --> F["settings 持久化"]
    E --> G["session metadata / permission mode 通知"]
    E --> H["env / auth cache reset"]
    E --> I["tmux / 宿主状态同步"]

这张图最想说明的是:

AppState 不是“UI 订阅 runtime”,而是 UI 和 runtime 共用同一个边界容器。

4. AppStateStore 里到底混了什么

源码锚点:

  • src/state/AppStateStore.ts

getDefaultAppState() 已经把这层的真实语义暴露得很明显了。

4.1 会话核心配置

这里装的是当前会话真正会影响执行面的设置,例如:

  • settings
  • mainLoopModel
  • toolPermissionContext
  • verbose
  • thinkingEnabled
  • promptSuggestionEnabled
  • authVersion

这些字段并不是“界面怎么画”,而是会直接改模型调用、权限判断和行为策略。

4.2 运行时注册表

这部分更像 runtime registry:

  • tasks
  • foregroundedTaskId
  • viewingAgentTaskId
  • agentNameRegistry
  • todos
  • mcp.clients / tools / commands / resources
  • plugins.enabled / disabled / errors / installationStatus / needsRefresh

这些状态的共同点是:

  • 会被多个子系统读写
  • 会驱动 UI 展示
  • 但本质不属于 UI

4.3 宿主投影状态

也有一批字段明显只是宿主层投影:

  • expandedView
  • footerSelection
  • viewSelectionMode
  • selectedIPAgentIndex
  • showRemoteCallout
  • statusLineText

这些字段如果以后换成 IDE 宿主、Web 宿主,很多都不该进入 session core。

4.4 临时工作流状态

还有一些状态带着明显的“流程编排”味道:

  • sessionHooks
  • promptSuggestion
  • speculation
  • activeOverlays
  • pendingPlanVerification
  • denialTracking

这类字段很容易在长期演化里把 store 越养越胖。

但反过来看,它也说明一件事:

Claude Code 习惯把“需要跨组件、跨子系统一起观察的临时流程状态”挂到统一 store,而不是藏进某个组件本地 hook。

5. 读写链路:为什么它不像普通 React state

源码锚点:

  • src/state/AppState.tsx
  • src/state/selectors.ts

AppStateProvider 并没有用常见的 useReducer + context value 全量往下灌。

它做的是:

  • 只创建一次 store
  • 用 selector 订阅切片
  • 基于 useSyncExternalStore 提供稳定订阅语义

这代表作者已经默认把它当成:

  • 需要多方读写
  • 不能因为一次无关更新就全树重渲染
  • 要和 React 生命周期适度解耦

换句话说,这里的主角不是 React,而是 store。

React 只是一个观察者宿主。

6. onChangeAppState() 才是真正的边界桥

源码锚点:

  • src/state/onChangeAppState.ts

如果只看名字,onChangeAppState() 很容易被误会成一个普通监听器。

但它其实承担了几类关键副作用:

6.1 把 runtime 选择写回外部世界

例如:

  • mainLoopModel 变化时写回 settings 和 bootstrap override
  • expandedViewverbose 变化时写回全局配置

6.2 把权限模式同步给外部会话协议

比如:

  • notifySessionMetadataChanged()
  • notifyPermissionModeChanged()

这说明权限模式不是 UI 自己的 toggle,而是 session metadata 的一部分。

6.3 在 settings 变化时重绑运行时

例如:

  • 清理 auth 相关 cache
  • 重新应用环境变量

也就是说,settings 不是“显示偏好”而已,它会触发 runtime rebinding。

7. 这块最值钱的地方:它揭示了 Claude Code 的宿主边界

如果把 AppState 只看成“做得不够干净”,会错过真正值钱的东西。

它其实回答了一个更重要的问题:

Claude Code 认为什么东西需要被宿主实时观察和操控。

现在被放进 AppState 的,通常满足至少一个条件:

  • 要直接投影到 REPL
  • 要被命令、工具、插件或 bridge 共同读写
  • 改动后需要立刻触发副作用
  • 当前还没有更稳定的专用边界

所以它不是随便长出来的“大对象”,而是历史演化下的“统一接缝层”。

8. 对 C# 拆分最有用的建议

这一章最不建议做的事,就是照着把整个 AppState 原样翻译成一个大 C# record。

更合理的拆法应该至少分四块:

8.1 SessionCoreState

放真正影响 agent 行为的字段:

  • effective settings
  • model selection
  • permission mode
  • auth version

8.2 RuntimeRegistries

放会被多个 runtime 子系统共享的注册表:

  • tasks
  • agents
  • mcp
  • plugins

8.3 HostProjectionState

只给 CLI / IDE / Web 宿主看的投影层:

  • panel selection
  • expanded view
  • status line
  • overlay visibility

8.4 StateSideEffectBridge

专门处理副作用:

  • settings persistence
  • session metadata notify
  • env reload
  • cache invalidation

9. 哪些值得抄,哪些不要抄

值得抄的:

  • selector + 外部 store 的组织方式
  • 把状态变化副作用集中到桥接层
  • 把 UI 投影和 runtime 共享态放进同一观察面

不要原样抄的:

  • 过多临时流程状态长期滞留在总 store
  • 宿主状态和核心状态没有显式类型边界
  • 让一个对象既负责存状态,又隐式代表一堆跨子系统协议

10. 一句话收口

AppState 真正值钱的地方,不是它装了多少字段,而是它暴露了 Claude Code 把 UI、宿主和 runtime 接在一起的那条真实接缝。