本章总览
本章拆解 state/ 核心三件套:store.ts(约 35 行)实现通用订阅存储;AppStateStore.ts(约 570 行)定义 AppState 巨型类型与 getDefaultAppState;AppState.tsx(约 200 行)提供 AppStateProvider、useAppState / useSetAppState / useAppStateStore。读完后应能解释:为何 Provider 用稳定 store 实例、为何 selector 不能返回新对象、以及 getDefaultAppState 里 lazy require teammate 的原因。
学完本章你应该能
- 说明 createStore 的 Object.is 短路语义
- 列举 AppState 主要字段簇及其消费方
- 描述 AppStateProvider 挂载时的 bypass 权限修正
- 理解 useSettingsChange 如何把磁盘 settings 推入 AppState
- 区分 useAppState 与 useAppStateMaybeOutsideOfProvider 的使用场景
- 能在 main.tsx 找到 headless createStore 与 onChangeAppState 接线
核心概念(先读懂这些)
store 是框架无关的最小内核
createStore 不依赖 React。headless SDK、MCP entrypoint、print.ts 可直接 createStore(getDefaultAppState(), onChangeAppState) 获得与 REPL 相同的 setState 语义。subscribe 是 Set,每次 setState 遍历通知——Ink 组件通过 useSyncExternalStore 桥接。
AppState 类型即产品状态机文档
AppStateStore.ts 内联注释密度极高:每个字段说明写入者、读者、是否持久化。例如 replBridge* 七字段描述 always-on bridge 状态机;teamContext 区分 swarm 多进程与 toolUseContext.agentId 的 in-process subagent。改字段前先读注释,避免把 session-only 数据误 persist。
Provider 嵌套禁止
HasAppStateContext 标记防止 AppStateProvider 嵌套——嵌套会导致双 store、权限与 MCP 状态分裂。测试里若需隔离,应 mount 独立 Provider 树而非嵌套。
建议学习步骤
- 阅读 store.ts createStore 完整实现
- 浏览 AppState 类型定义中的 teamContext / mcp / tasks 簇
- 阅读 getDefaultAppState 的 initialMode 与 teammate lazy require
- 阅读 AppStateProvider 的 bypass 修正与 useSettingsChange
- 阅读 useAppState 的 useSyncExternalStore 与 selector 注释
- 在 main.tsx 搜索 createStore 与 AppStateProvider
常见误区
注意
从 AppState.tsx import 类型会拉入 React——.ts 文件应 import AppStateStore.js
注意
getDefaultAppState 缺省不含 teamContext——main.tsx computeInitialTeamContext 补全
注意
setState 返回 prev 引用时 onChange 与 listeners 均不触发
注意
VoiceProvider 用 feature DCE——外部构建是 passthrough children
在架构中的位置
AppState 数据流:
getDefaultAppState() + main.tsx 补丁(teamContext、kairosEnabled…)
→ createStore(initial, onChangeAppState)
→ AppStateProvider 或 headless 裸 store
→ useAppState(s => s.field) / store.getState() 非 React 读取
→ setState(prev => ({ ...prev, field: x }))
→ onChangeAppState → settings / globalConfig / bootstrap override
REPL.tsx 是最大消费者:数十个 useAppState 切片避免整树重渲染。QueryEngine、Tool 执行通过 closure 里的 getState() 读最新 mcp.tools、toolPermissionContext。
createStore 实现要点
store.ts 刻意保持无依赖:
- getState 闭包捕获
state变量,不暴露 setter - setState 先 updater(prev),再 Object.is 比较;相等则完全 no-op(不调 onChange、不 notify)
- subscribe 返回 unsubscribe 函数
这种语义与 Zustand / Redux 的「浅比较 bail-out」一致,但 AppState 体量大,依赖 Object.is 要求 updater 在不变时 return prev——teammateViewHelpers 中大量 if (!needsView) return prev 模式即为此。
onChange 回调可选,REPL 与 headless 均传入 onChangeAppState 做跨层同步。
源码引用: src/state/store.ts · 第 1–34 行(共 35 行)
1| type Listener = () => void
2| type OnChange<T> = (args: { newState: T; oldState: T }) => void
3|
4| export type Store<T> = {
5| getState: () => T
6| setState: (updater: (prev: T) => T) => void
7| subscribe: (listener: Listener) => () => void
8| }
9|
10| export function createStore<T>(
11| initialState: T,
12| onChange?: OnChange<T>,
13| ): Store<T> {
14| let state = initialState
15| const listeners = new Set<Listener>()
16|
17| return {
18| getState: () => state,
19|
20| setState: (updater: (prev: T) => T) => {
21| const prev = state
22| const next = updater(prev)
23| if (Object.is(next, prev)) return
24| state = next
25| onChange?.({ newState: next, oldState: prev })
26| for (const listener of listeners) listener()
27| },
28|
29| subscribe: (listener: Listener) => {
30| listeners.add(listener)
31| return () => listeners.delete(listener)
32| },
33| }
34| }
AppState 类型:DeepImmutable 与例外
AppState 声明为 DeepImmutable<{ ... }> & { tasks; agentNameRegistry; ... }。
DeepImmutable 递归 readonly,防止组件意外 mutate nested 对象。但 tasks 含 TaskState 函数(abortController、onDone),agentNameRegistry 是 Map,sessionHooks 是 Map——这些被 union 排除在 DeepImmutable 外。
重要嵌套对象:
| 字段 | 结构 | 备注 |
|---|---|---|
| toolPermissionContext | ToolPermissionContext | mode、rules、bypass 标志 |
| mcp | clients/tools/commands/resources | pluginReconnectKey 仅作 effect 依赖 |
| teamContext | teammates 字典 + leadAgentId | swarm 多进程身份 |
| speculation | idle / active 判别联合 | active 含 abort、messagesRef |
| promptSuggestion | text + promptId + 时间戳 | 与 PromptSuggestion 服务联动 |
CompletionBoundary 与 SpeculationState 也在此文件导出,供 compact / speculation 服务使用。
源码引用: src/state/AppStateStore.ts · 第 41–88 行(共 570 行)
41| export type CompletionBoundary =
42| | { type: 'complete'; completedAt: number; outputTokens: number }
43| | { type: 'bash'; command: string; completedAt: number }
44| | { type: 'edit'; toolName: string; filePath: string; completedAt: number }
45| | {
46| type: 'denied_tool'
47| toolName: string
48| detail: string
49| completedAt: number
50| }
51|
52| export type SpeculationResult = {
53| messages: Message[]
54| boundary: CompletionBoundary | null
55| timeSavedMs: number
56| }
57|
58| export type SpeculationState =
59| | { status: 'idle' }
60| | {
61| status: 'active'
62| id: string
63| abort: () => void
64| startTime: number
65| messagesRef: { current: Message[] } // Mutable ref - avoids array spreading per message
66| writtenPathsRef: { current: Set<string> } // Mutable ref - relative paths written to overlay
67| boundary: CompletionBoundary | null
68| suggestionLength: number
69| toolUseCount: number
70| isPipelined: boolean
71| contextRef: { current: REPLHookContext }
72| pipelinedSuggestion?: {
73| text: string
74| promptId: 'user_intent' | 'stated_intent'
75| generationRequestId: string | null
76| } | null
77| }
78|
79| export const IDLE_SPECULATION_STATE: SpeculationState = { status: 'idle' }
80|
81| export type FooterItem =
82| | 'tasks'
83| | 'tmux'
84| | 'bagel'
85| | 'teams'
86| | 'bridge'
87| | 'companion'
88|
源码引用: src/state/AppStateStore.ts · 第 89–158 行(共 570 行)
89| export type AppState = DeepImmutable<{
90| settings: SettingsJson
91| verbose: boolean
92| mainLoopModel: ModelSetting
93| mainLoopModelForSession: ModelSetting
94| statusLineText: string | undefined
95| expandedView: 'none' | 'tasks' | 'teammates'
96| isBriefOnly: boolean
97| // Optional - only present when ENABLE_AGENT_SWARMS is true (for dead code elimination)
98| showTeammateMessagePreview?: boolean
99| selectedIPAgentIndex: number
100| // CoordinatorTaskPanel selection: -1 = pill, 0 = main, 1..N = agent rows.
101| // AppState (not local) so the panel can read it directly without prop-drilling
102| // through PromptInput → PromptInputFooter.
103| coordinatorTaskIndex: number
104| viewSelectionMode: 'none' | 'selecting-agent' | 'viewing-agent'
105| // Which footer pill is focused (arrow-key navigation below the prompt).
106| // Lives in AppState so pill components rendered outside PromptInput
107| // (CompanionSprite in REPL.tsx) can read their own focused state.
108| footerSelection: FooterItem | null
109| toolPermissionContext: ToolPermissionContext
110| spinnerTip?: string
111| // Agent name from --agent CLI flag or settings (for logo display)
112| agent: string | undefined
113| // Assistant mode fully enabled (settings + GrowthBook gate + trust).
114| // Single source of truth - computed once in main.tsx before option
115| // mutation, consumers read this instead of re-calling isAssistantMode().
116| kairosEnabled: boolean
117| // Remote session URL for --remote mode (shown in footer indicator)
118| remoteSessionUrl: string | undefined
119| // Remote session WS state (`claude assistant` viewer). 'connected' means the
120| // live event stream is open; 'reconnecting' = transient WS drop, backoff
121| // in progress; 'disconnected' = permanent close or reconnects exhausted.
122| remoteConnectionStatus:
123| | 'connecting'
124| | 'connected'
125| | 'reconnecting'
126| | 'disconnected'
127| // `claude assistant`: count of background tasks (Agent calls, teammates,
128| // workflows) running inside the REMOTE daemon child. Event-sourced from
129| // system/task_started and system/task_notification on the WS. The local
130| // AppState.tasks is always empty in viewer mode — the tasks live in a
131| // different process.
132| remoteBackgroundTaskCount: number
133| // Always-on bridge: desired state (controlled by /config or footer toggle)
134| replBridgeEnabled: boolean
135| // Always-on bridge: true when activated via /remote-control command, false when config-driven
136| replBridgeExplicit: boolean
137| // Outbound-only mode: forward events to CCR but reject inbound prompts/control
138| replBridgeOutboundOnly: boolean
139| // Always-on bridge: env registered + session created (= "Ready")
140| replBridgeConnected: boolean
141| // Always-on bridge: ingress WebSocket is open (= "Connected" - user on claude.ai)
142| replBridgeSessionActive: boolean
143| // Always-on bridge: poll loop is in error backoff (= "Reconnecting")
144| replBridgeReconnecting: boolean
145| // Always-on bridge: connect URL for Ready state (?bridge=envId)
146| replBridgeConnectUrl: string | undefined
147| // Always-on bridge: session URL on claude.ai (set when connected)
148| replBridgeSessionUrl: string | undefined
149| // Always-on bridge: IDs for debugging (shown in dialog when --verbose)
150| replBridgeEnvironmentId: string | undefined
151| replBridgeSessionId: string | undefined
152| // Always-on bridge: error message when connection fails (shown in BridgeDialog)
153| replBridgeError: string | undefined
154| // Always-on bridge: session name set via `/remote-control <name>` (used as session title)
155| replBridgeInitialName: string | undefined
156| // Always-on bridge: first-time remote dialog pending (set by /remote-control command)
157| showRemoteCallout: boolean
158| }> & {
源码引用: src/state/AppStateStore.ts · 第 323–345 行(共 570 行)
323| teamContext?: {
324| teamName: string
325| teamFilePath: string
326| leadAgentId: string
327| // Self-identity for swarm members (separate processes in tmux panes)
328| // Note: This is different from toolUseContext.agentId which is for in-process subagents
329| selfAgentId?: string // Swarm member's own ID (same as leadAgentId for leaders)
330| selfAgentName?: string // Swarm member's name ('team-lead' for leaders)
331| isLeader?: boolean // True if this swarm member is the team leader
332| selfAgentColor?: string // Assigned color for UI (used by dynamically joined sessions)
333| teammates: {
334| [teammateId: string]: {
335| name: string
336| agentType?: string
337| color?: string
338| tmuxSessionName: string
339| tmuxPaneId: string
340| cwd: string
341| worktreePath?: string
342| spawnedAt: number
343| }
344| }
345| }
源码引用: src/state/AppStateStore.ts · 第 173–216 行(共 570 行)
173| mcp: {
174| clients: MCPServerConnection[]
175| tools: Tool[]
176| commands: Command[]
177| resources: Record<string, ServerResource[]>
178| /**
179| * Incremented by /reload-plugins to trigger MCP effects to re-run
180| * and pick up newly-enabled plugin MCP servers. Effects read this
181| * as a dependency; the value itself is not consumed.
182| */
183| pluginReconnectKey: number
184| }
185| plugins: {
186| enabled: LoadedPlugin[]
187| disabled: LoadedPlugin[]
188| commands: Command[]
189| /**
190| * Plugin system errors collected during loading and initialization.
191| * See {@link PluginError} type documentation for complete details on error
192| * structure, context fields, and display format.
193| */
194| errors: PluginError[]
195| // Installation status for background plugin/marketplace installation
196| installationStatus: {
197| marketplaces: Array<{
198| name: string
199| status: 'pending' | 'installing' | 'installed' | 'failed'
200| error?: string
201| }>
202| plugins: Array<{
203| id: string
204| name: string
205| status: 'pending' | 'installing' | 'installed' | 'failed'
206| error?: string
207| }>
208| }
209| /**
210| * Set to true when plugin state on disk has changed (background reconcile,
211| * /plugin menu install, external settings edit) and active components are
212| * stale. In interactive mode, user runs /reload-plugins to consume. In
213| * headless mode, refreshPluginState() auto-consumes via refreshActivePlugins().
214| */
215| needsRefresh: boolean
216| }
getDefaultAppState 默认值策略
getDefaultAppState 构造「空 REPL」快照:
- settings: getInitialSettings() 合并 user/project/local/policy
- mainLoopModel: null(表示默认模型,非显式 override)
- toolPermissionContext.mode: teammate 且 plan_mode_required 时为
'plan',否则'default'——lazy require../utils/teammate.js破循环依赖 - mcp / plugins: 空数组,needsRefresh false
- thinkingEnabled / promptSuggestionEnabled: 各自 shouldEnable* 函数
- speculation: IDLE_SPECULATION_STATE
- replBridge*: 全 false/undefined
未在默认值中出现的可选字段(teamContext、computerUseMcpState、tungsten*)由 main.tsx 或 feature gate 在 initialState 补丁。resume 路径从 transcript + sessionStorage 恢复 messages,AppState 仍从 getDefaultAppState 基底 merge。
源码引用: src/state/AppStateStore.ts · 第 456–503 行(共 570 行)
456| export function getDefaultAppState(): AppState {
457| // Determine initial permission mode for teammates spawned with plan_mode_required
458| // Use lazy require to avoid circular dependency with teammate.ts
459| /* eslint-disable @typescript-eslint/no-require-imports */
460| const teammateUtils =
461| require('../utils/teammate.js') as typeof import('../utils/teammate.js')
462| /* eslint-enable @typescript-eslint/no-require-imports */
463| const initialMode: PermissionMode =
464| teammateUtils.isTeammate() && teammateUtils.isPlanModeRequired()
465| ? 'plan'
466| : 'default'
467|
468| return {
469| settings: getInitialSettings(),
470| tasks: {},
471| agentNameRegistry: new Map(),
472| verbose: false,
473| mainLoopModel: null, // alias, full name (as with --model or env var), or null (default)
474| mainLoopModelForSession: null,
475| statusLineText: undefined,
476| expandedView: 'none',
477| isBriefOnly: false,
478| showTeammateMessagePreview: false,
479| selectedIPAgentIndex: -1,
480| coordinatorTaskIndex: -1,
481| viewSelectionMode: 'none',
482| footerSelection: null,
483| kairosEnabled: false,
484| remoteSessionUrl: undefined,
485| remoteConnectionStatus: 'connecting',
486| remoteBackgroundTaskCount: 0,
487| replBridgeEnabled: false,
488| replBridgeExplicit: false,
489| replBridgeOutboundOnly: false,
490| replBridgeConnected: false,
491| replBridgeSessionActive: false,
492| replBridgeReconnecting: false,
493| replBridgeConnectUrl: undefined,
494| replBridgeSessionUrl: undefined,
495| replBridgeEnvironmentId: undefined,
496| replBridgeSessionId: undefined,
497| replBridgeError: undefined,
498| replBridgeInitialName: undefined,
499| showRemoteCallout: false,
500| toolPermissionContext: {
501| ...getEmptyToolPermissionContext(),
502| mode: initialMode,
503| },
源码引用: src/state/AppStateStore.ts · 第 512–568 行(共 570 行)
512| mcp: {
513| clients: [],
514| tools: [],
515| commands: [],
516| resources: {},
517| pluginReconnectKey: 0,
518| },
519| plugins: {
520| enabled: [],
521| disabled: [],
522| commands: [],
523| errors: [],
524| installationStatus: {
525| marketplaces: [],
526| plugins: [],
527| },
528| needsRefresh: false,
529| },
530| todos: {},
531| remoteAgentTaskSuggestions: [],
532| notifications: {
533| current: null,
534| queue: [],
535| },
536| elicitation: {
537| queue: [],
538| },
539| thinkingEnabled: shouldEnableThinkingByDefault(),
540| promptSuggestionEnabled: shouldEnablePromptSuggestion(),
541| sessionHooks: new Map(),
542| inbox: {
543| messages: [],
544| },
545| workerSandboxPermissions: {
546| queue: [],
547| selectedIndex: 0,
548| },
549| pendingWorkerRequest: null,
550| pendingSandboxRequest: null,
551| promptSuggestion: {
552| text: null,
553| promptId: null,
554| shownAt: 0,
555| acceptedAt: 0,
556| generationRequestId: null,
557| },
558| speculation: IDLE_SPECULATION_STATE,
559| speculationSessionTimeSavedMs: 0,
560| skillImprovement: {
561| suggestion: null,
562| },
563| authVersion: 0,
564| initialMessage: null,
565| effortValue: undefined,
566| activeOverlays: new Set<string>(),
567| fastMode: false,
568| }
AppStateProvider 与 React Hooks
AppStateProvider 职责:
- 禁止嵌套 — hasAppStateContext 检测
- 稳定 store — useState(() => createStore(...)) 只运行一次
- Mount 修正 bypass — 若 remote settings 在 Provider 挂载前禁用 bypass,useEffect 一次性 setState 为 createDisabledBypassPermissionsContext
- settings 文件监听 — useSettingsChange + applySettingsChange 把磁盘变更写入 AppState
- 上下文包装 — MailboxProvider、VoiceProvider(feature gated)
useAppState(selector) 用 useSyncExternalStore(store.subscribe, get, get)——SSR 与 client 同一 get 快照。
useSetAppState 只取 store.setState,不订阅,适合纯写入组件。
useAppStateStore 暴露完整 store 给需要 getState 的子树。
useAppStateMaybeOutsideOfProvider 在无 Provider 时返回 undefined 而非 throw——Doctor 等可选挂载场景。
源码引用: src/state/AppState.tsx · 第 37–109 行(共 201 行)
37| export {
38| type AppState,
39| type AppStateStore,
40| type CompletionBoundary,
41| getDefaultAppState,
42| IDLE_SPECULATION_STATE,
43| type SpeculationResult,
44| type SpeculationState,
45| } from './AppStateStore.js'
46|
47| export const AppStoreContext = React.createContext<AppStateStore | null>(null)
48|
49| type Props = {
50| children: React.ReactNode
51| initialState?: AppState
52| onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void
53| }
54|
55| const HasAppStateContext = React.createContext<boolean>(false)
56|
57| export function AppStateProvider({
58| children,
59| initialState,
60| onChangeAppState,
61| }: Props): React.ReactNode {
62| // Don't allow nested AppStateProviders.
63| const hasAppStateContext = useContext(HasAppStateContext)
64| if (hasAppStateContext) {
65| throw new Error(
66| 'AppStateProvider can not be nested within another AppStateProvider',
67| )
68| }
69|
70| // Store is created once and never changes -- stable context value means
71| // the provider never triggers re-renders. Consumers subscribe to slices
72| // via useSyncExternalStore in useAppState(selector).
73| const [store] = useState(() =>
74| createStore<AppState>(
75| initialState ?? getDefaultAppState(),
76| onChangeAppState,
77| ),
78| )
79|
80| // Check on mount if bypass mode should be disabled
81| // This handles the race condition where remote settings load BEFORE this component mounts,
82| // meaning the settings change notification was sent when no listeners were subscribed.
83| // On subsequent sessions, the cached remote-settings.json is read during initial setup,
84| // but on the first session the remote fetch may complete before React mounts.
85| useEffect(() => {
86| const { toolPermissionContext } = store.getState()
87| if (
88| toolPermissionContext.isBypassPermissionsModeAvailable &&
89| isBypassPermissionsModeDisabled()
90| ) {
91| logForDebugging(
92| 'Disabling bypass permissions mode on mount (remote settings loaded before mount)',
93| )
94| store.setState(prev => ({
95| ...prev,
96| toolPermissionContext: createDisabledBypassPermissionsContext(
97| prev.toolPermissionContext,
98| ),
99| }))
100| }
101| // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
102| }, [])
103|
104| // Listen for external settings changes and sync to AppState.
105| // This ensures file watcher changes propagate through the app --
106| // shared with the headless/SDK path via applySettingsChange.
107| const onSettingsChange = useEffectEvent((source: SettingSource) =>
108| applySettingsChange(source, store.setState),
109| )
源码引用: src/state/AppState.tsx · 第 126–163 行(共 201 行)
126| if (!store) {
127| throw new ReferenceError(
128| 'useAppState/useSetAppState cannot be called outside of an <AppStateProvider />',
129| )
130| }
131| return store
132| }
133|
134| /**
135| * Subscribe to a slice of AppState. Only re-renders when the selected value
136| * changes (compared via Object.is).
137| *
138| * For multiple independent fields, call the hook multiple times:
139| * ```
140| * const verbose = useAppState(s => s.verbose)
141| * const model = useAppState(s => s.mainLoopModel)
142| * ```
143| *
144| * Do NOT return new objects from the selector -- Object.is will always see
145| * them as changed. Instead, select an existing sub-object reference:
146| * ```
147| * const { text, promptId } = useAppState(s => s.promptSuggestion) // good
148| * ```
149| */
150| export function useAppState<T>(selector: (state: AppState) => T): T {
151| const store = useAppStore()
152|
153| const get = () => {
154| const state = store.getState()
155| const selected = selector(state)
156|
157| if ("external" === 'ant' && state === selected) {
158| throw new Error(
159| `Your selector in \`useAppState(${selector.toString()})\` returned the original state, which is not allowed. You must instead return a property for optimised rendering.`,
160| )
161| }
162|
163| return selected
源码引用: src/state/AppState.tsx · 第 170–199 行(共 201 行)
170| * Get the setAppState updater without subscribing to any state.
171| * Returns a stable reference that never changes -- components using only
172| * this hook will never re-render from state changes.
173| */
174| export function useSetAppState(): (
175| updater: (prev: AppState) => AppState,
176| ) => void {
177| return useAppStore().setState
178| }
179|
180| /**
181| * Get the store directly (for passing getState/setState to non-React code).
182| */
183| export function useAppStateStore(): AppStateStore {
184| return useAppStore()
185| }
186|
187| const NOOP_SUBSCRIBE = () => () => {}
188|
189| /**
190| * Safe version of useAppState that returns undefined if called outside of AppStateProvider.
191| * Useful for components that may be rendered in contexts where AppStateProvider isn't available.
192| */
193| export function useAppStateMaybeOutsideOfProvider<T>(
194| selector: (state: AppState) => T,
195| ): T | undefined {
196| const store = useContext(AppStoreContext)
197| return useSyncExternalStore(store ? store.subscribe : NOOP_SUBSCRIBE, () =>
198| store ? selector(store.getState()) : undefined,
199| )
headless 与 REPL 初始状态差异
main.tsx 在渲染 REPL 前计算 initialState 补丁:
- kairosEnabled、agent(--agent)、teamContext(computeInitialTeamContext)
- mcp 预连接结果、plugins 列表
- initialMessage(CLI -p / plan exit)
- remote / bridge 标志位
Headless 路径(print.ts、SDK):
headlessInitialState = { ...getDefaultAppState(), ...patches }
headlessStore = createStore(headlessInitialState, onChangeAppState)
无 Provider,但 onChangeAppState 仍同步 model 到 settings 与 bootstrap。调试 headless 与 interactive 行为差异时,对比 initialState 补丁而非 getDefaultAppState 本身。
源码引用: src/main.tsx · 第 2650–2655 行(共 6604 行)
2650| // that need MCP pass --mcp-config explicitly.
2651| !isBareMode()
2652| ? fetchClaudeAIMcpConfigsIfEligible().then(configs => {
2653| const { allowed, blocked } = filterMcpServersByPolicy(configs)
2654| if (blocked.length > 0) {
2655| process.stderr.write(
迁移注释与 import hygiene
AppState.tsx 顶部 TODO:类型 re-export 仅为 back-compat,新代码应 import type { AppState } from './AppStateStore.js' 避免 .ts 文件依赖 React compiler runtime。
React Compiler(_c memo cache)已应用于 Provider 与 hooks——阅读源码时注意 $[n] 槽位是编译器产物,非手写 useMemo。
export 从 AppState.tsx:AppState、AppStateStore、CompletionBoundary、SpeculationState、IDLE_SPECULATION_STATE、getDefaultAppState。工具链 grep 时应同时搜 AppStateStore.ts 与 AppState.tsx。
本章小结与延伸
app-state-core = 订阅存储 + 类型定义 + React 绑定。下一章 app-state-selectors,读纯函数 selector 与 onChangeAppState 副作用。 继续学习: