Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

selectors.ts 提供纯函数,从 AppState 派生「当前查看的队友」「输入应路由到谁」等计算态;onChangeAppState.ts 是 setState 的 唯一集中副作用钩子,把 permission mode、model、verbose、settings 等变更同步到 globalConfig、userSettings、bootstrap override 与 CCR metadata。本章要求你能画出:任意 setAppState 调用如何触发 onChange diff,以及为何 permission mode 必须在此 choke point 通知 CCR。

学完本章你应该能

  • 解释 getViewedTeammateTask 的三重 guard
  • 说明 getActiveAgentForInput 判别联合的三种 type
  • 描述 onChangeAppState 对 toolPermissionContext.mode 的 diff 逻辑
  • 理解 mainLoopModel null vs 非 null 的双向 settings 同步
  • 说出 externalMetadataToAppState 的逆向恢复用途
  • 列举 settings 变更时清 auth cache 的原因

核心概念(先读懂这些)

selector 无副作用

selectors.ts 注释强调:keep selectors pure — 只读 AppState,不写磁盘、不调 API。副作用 belong in onChangeAppState 或 caller。这允许在 query 循环、测试里直接调用 getActiveAgentForInput(appState) 而不 mount React。

onChange 是跨系统事件总线

onChangeAppState 不是 Redux middleware 的泛型管道,而是针对已知字段的显式 diff 列表。新增需要「AppState 变更 → 外部系统」同步的字段时,应在此追加 if 块,而非在 20 个 setState 调用点各写一遍——permission mode 的 refactor 注释说明了 scattered callsites 曾导致 CCR stale 的教训。

external vs internal permission mode

toExternalPermissionMode 过滤 bubble、ungated auto 等内部模式名。CCR notify 仅在外部模式变化时发送;SDK notifyPermissionModeChanged 传 raw mode,print.ts listener 自行过滤。Ultraplan 首次进入 plan 时 is_ultraplan_mode 原子写入 metadata。

建议学习步骤

  1. 阅读 getViewedTeammateTask 逐步 guard
  2. 阅读 getActiveAgentForInput 分支顺序
  3. 阅读 onChangeAppState permission mode 块注释
  4. 阅读 mainLoopModel settings 双向同步
  5. 阅读 expandedView → globalConfig 映射
  6. 阅读 settings 变更 auth cache 清理

常见误区

注意

getViewedTeammateTask 只认 InProcessTeammateTask,local_agent 走另一分支

注意

onChange 比较 settings 用 !== 引用相等——mutate settings 对象不会触发

注意

tungstenPanelVisible 持久化仅 USER_TYPE === ant

注意

externalMetadataToAppState 供 worker restart 恢复,非日常路径

在架构中的位置

selector 与 onChange 在 REPL 输入路径中的位置:

用户按键 / 提交
  → getActiveAgentForInput(store.getState())
       ├─ leader → 主 query 队列
       ├─ viewed → InProcessTeammate pendingUserMessages
       └─ named_agent → LocalAgentTask 路由
  → setAppState(...)
  → onChangeAppState diff
       ├─ notifySessionMetadataChanged (CCR)
       ├─ saveGlobalConfig / updateSettingsForSource
       └─ setMainLoopModelOverride (bootstrap)

processTextPrompt、PromptInput、bridge control_request 均可能 setState;onChange 保证外部观察者不必 hook 每个 callsite。

getViewedTeammateTask

输入:Pick<AppState, 'viewingAgentTaskId' | 'tasks'>。

返回 InProcessTeammateTaskState 或 undefined。Guard 顺序:

  1. viewingAgentTaskId falsy → undefined(leader 视图)
  2. tasks[id] 不存在 → undefined(已被 evict)
  3. !isInProcessTeammateTask(task) → undefined(可能是 local_agent)

用途:BackgroundTasksDialog 渲染 viewed transcript、useTeammateViewAutoExit 窄化 status、getActiveAgentForInput 优先分支。

与 viewSelectionMode 配合:enterTeammateView 设为 'viewing-agent';exit 恢复 'none'。selector 不读 viewSelectionMode——task 类型才是路由真相源。

源码引用: src/state/selectors.ts · 第 11–40 行(共 77 行)

  11| /**
  12|  * Get the currently viewed teammate task, if any.
  13|  * Returns undefined if:
  14|  * - No teammate is being viewed (viewingAgentTaskId is undefined)
  15|  * - The task ID doesn't exist in tasks
  16|  * - The task is not an in-process teammate task
  17|  */
  18| export function getViewedTeammateTask(
  19|   appState: Pick<AppState, 'viewingAgentTaskId' | 'tasks'>,
  20| ): InProcessTeammateTaskState | undefined {
  21|   const { viewingAgentTaskId, tasks } = appState
  22| 
  23|   // Not viewing any teammate
  24|   if (!viewingAgentTaskId) {
  25|     return undefined
  26|   }
  27| 
  28|   // Look up the task
  29|   const task = tasks[viewingAgentTaskId]
  30|   if (!task) {
  31|     return undefined
  32|   }
  33| 
  34|   // Verify it's an in-process teammate task
  35|   if (!isInProcessTeammateTask(task)) {
  36|     return undefined
  37|   }
  38| 
  39|   return task
  40| }

getActiveAgentForInput 路由判别

返回 ActiveAgentForInput 联合类型:

type含义典型场景
leader输入进主 REPL 队列默认
viewed输入进 in-process teammate用户按 → 查看队友 transcript
named_agent输入进 LocalAgentTaskforeground local agent(非 in-process teammate 类型)

算法:

  1. 先 getViewedTeammateTask — 命中则 { type: 'viewed', task }
  2. 否则若 viewingAgentTaskId 指向 local_agent → named_agent
  3. 默认 leader

注意:viewingAgentTaskId 同时服务 in-process teammate 与 local_agent,类型 guard 决定路由。错误窄化会导致消息进错队列(teammate 收不到 user ping)。

源码引用: src/state/selectors.ts · 第 42–76 行(共 77 行)

  42| /**
  43|  * Return type for getActiveAgentForInput selector.
  44|  * Discriminated union for type-safe input routing.
  45|  */
  46| export type ActiveAgentForInput =
  47|   | { type: 'leader' }
  48|   | { type: 'viewed'; task: InProcessTeammateTaskState }
  49|   | { type: 'named_agent'; task: LocalAgentTaskState }
  50| 
  51| /**
  52|  * Determine where user input should be routed.
  53|  * Returns:
  54|  * - { type: 'leader' } when not viewing a teammate (input goes to leader)
  55|  * - { type: 'viewed', task } when viewing an agent (input goes to that agent)
  56|  *
  57|  * Used by input routing logic to direct user messages to the correct agent.
  58|  */
  59| export function getActiveAgentForInput(
  60|   appState: AppState,
  61| ): ActiveAgentForInput {
  62|   const viewedTask = getViewedTeammateTask(appState)
  63|   if (viewedTask) {
  64|     return { type: 'viewed', task: viewedTask }
  65|   }
  66| 
  67|   const { viewingAgentTaskId, tasks } = appState
  68|   if (viewingAgentTaskId) {
  69|     const task = tasks[viewingAgentTaskId]
  70|     if (task?.type === 'local_agent') {
  71|       return { type: 'named_agent', task }
  72|     }
  73|   }
  74| 
  75|   return { type: 'leader' }
  76| }

onChangeAppState:permission mode 同步

核心 diff:

prevMode = oldState.toolPermissionContext.mode
newMode = newState.toolPermissionContext.mode
if prevMode !== newMode:
  prevExternal = toExternalPermissionMode(prevMode)
  newExternal = toExternalPermissionMode(newMode)
  if prevExternal !== newExternal:
    notifySessionMetadataChanged({ permission_mode, is_ultraplan_mode })
  notifyPermissionModeChanged(newMode)

注释列举曾遗漏的路径:Shift+Tab、ExitPlanMode 对话框、/plan、rewind、REPL bridge onSetPermissionMode。集中到 onChange 后这些路径零改动即同步 CCR。

is_ultraplan_mode:仅当外部 mode 变为 plan 且 newState.isUltraplanMode 从 false→true 时写 true,否则 null(RFC 7396 删 key)。

源码引用: src/state/onChangeAppState.ts · 第 43–92 行(共 172 行)

  43| export function onChangeAppState({
  44|   newState,
  45|   oldState,
  46| }: {
  47|   newState: AppState
  48|   oldState: AppState
  49| }) {
  50|   // toolPermissionContext.mode — single choke point for CCR/SDK mode sync.
  51|   //
  52|   // Prior to this block, mode changes were relayed to CCR by only 2 of 8+
  53|   // mutation paths: a bespoke setAppState wrapper in print.ts (headless/SDK
  54|   // mode only) and a manual notify in the set_permission_mode handler.
  55|   // Every other path — Shift+Tab cycling, ExitPlanModePermissionRequest
  56|   // dialog options, the /plan slash command, rewind, the REPL bridge's
  57|   // onSetPermissionMode — mutated AppState without telling
  58|   // CCR, leaving external_metadata.permission_mode stale and the web UI out
  59|   // of sync with the CLI's actual mode.
  60|   //
  61|   // Hooking the diff here means ANY setAppState call that changes the mode
  62|   // notifies CCR (via notifySessionMetadataChanged → ccrClient.reportMetadata)
  63|   // and the SDK status stream (via notifyPermissionModeChanged → registered
  64|   // in print.ts). The scattered callsites above need zero changes.
  65|   const prevMode = oldState.toolPermissionContext.mode
  66|   const newMode = newState.toolPermissionContext.mode
  67|   if (prevMode !== newMode) {
  68|     // CCR external_metadata must not receive internal-only mode names
  69|     // (bubble, ungated auto). Externalize first — and skip
  70|     // the CCR notify if the EXTERNAL mode didn't change (e.g.,
  71|     // default→bubble→default is noise from CCR's POV since both
  72|     // externalize to 'default'). The SDK channel (notifyPermissionModeChanged)
  73|     // passes raw mode; its listener in print.ts applies its own filter.
  74|     const prevExternal = toExternalPermissionMode(prevMode)
  75|     const newExternal = toExternalPermissionMode(newMode)
  76|     if (prevExternal !== newExternal) {
  77|       // Ultraplan = first plan cycle only. The initial control_request
  78|       // sets mode and isUltraplanMode atomically, so the flag's
  79|       // transition gates it. null per RFC 7396 (removes the key).
  80|       const isUltraplan =
  81|         newExternal === 'plan' &&
  82|         newState.isUltraplanMode &&
  83|         !oldState.isUltraplanMode
  84|           ? true
  85|           : null
  86|       notifySessionMetadataChanged({
  87|         permission_mode: newExternal,
  88|         is_ultraplan_mode: isUltraplan,
  89|       })
  90|     }
  91|     notifyPermissionModeChanged(newMode)
  92|   }

onChangeAppState:model 与 UI 偏好

mainLoopModel 双向同步:

  • 变为 null → updateSettingsForSource('userSettings', { model: undefined }) + setMainLoopModelOverride(null)
  • 变为 非 null → 写入 userSettings.model + setMainLoopModelOverride

这连接 AppState(UI 当前选择)与 bootstrap override(query 读模型)与磁盘 settings(跨 session)。

expandedView 映射 legacy globalConfig:

  • 'tasks' → showExpandedTodos true
  • 'teammates' → showSpinnerTree true
  • 其他 → 两者 false

verbose 直接 saveGlobalConfig。

tungstenPanelVisible ant-only 持久化到 globalConfig。

源码引用: src/state/onChangeAppState.ts · 第 94–152 行(共 172 行)

  94|   // mainLoopModel: remove it from settings?
  95|   if (
  96|     newState.mainLoopModel !== oldState.mainLoopModel &&
  97|     newState.mainLoopModel === null
  98|   ) {
  99|     // Remove from settings
 100|     updateSettingsForSource('userSettings', { model: undefined })
 101|     setMainLoopModelOverride(null)
 102|   }
 103| 
 104|   // mainLoopModel: add it to settings?
 105|   if (
 106|     newState.mainLoopModel !== oldState.mainLoopModel &&
 107|     newState.mainLoopModel !== null
 108|   ) {
 109|     // Save to settings
 110|     updateSettingsForSource('userSettings', { model: newState.mainLoopModel })
 111|     setMainLoopModelOverride(newState.mainLoopModel)
 112|   }
 113| 
 114|   // expandedView → persist as showExpandedTodos + showSpinnerTree for backwards compat
 115|   if (newState.expandedView !== oldState.expandedView) {
 116|     const showExpandedTodos = newState.expandedView === 'tasks'
 117|     const showSpinnerTree = newState.expandedView === 'teammates'
 118|     if (
 119|       getGlobalConfig().showExpandedTodos !== showExpandedTodos ||
 120|       getGlobalConfig().showSpinnerTree !== showSpinnerTree
 121|     ) {
 122|       saveGlobalConfig(current => ({
 123|         ...current,
 124|         showExpandedTodos,
 125|         showSpinnerTree,
 126|       }))
 127|     }
 128|   }
 129| 
 130|   // verbose
 131|   if (
 132|     newState.verbose !== oldState.verbose &&
 133|     getGlobalConfig().verbose !== newState.verbose
 134|   ) {
 135|     const verbose = newState.verbose
 136|     saveGlobalConfig(current => ({
 137|       ...current,
 138|       verbose,
 139|     }))
 140|   }
 141| 
 142|   // tungstenPanelVisible (ant-only tmux panel sticky toggle)
 143|   if (process.env.USER_TYPE === 'ant') {
 144|     if (
 145|       newState.tungstenPanelVisible !== oldState.tungstenPanelVisible &&
 146|       newState.tungstenPanelVisible !== undefined &&
 147|       getGlobalConfig().tungstenPanelVisible !== newState.tungstenPanelVisible
 148|     ) {
 149|       const tungstenPanelVisible = newState.tungstenPanelVisible
 150|       saveGlobalConfig(current => ({ ...current, tungstenPanelVisible }))
 151|     }
 152|   }

onChangeAppState:settings 与 auth cache

当 newState.settings !== oldState.settings(引用变化):

  1. clearApiKeyHelperCache()
  2. clearAwsCredentialsCache()
  3. clearGcpCredentialsCache()
  4. 若 settings.env 变 → applyConfigEnvironmentVariables()

保证用户在 /config 或外部编辑 settings.json 后,下一次 API 调用用新 apiKeyHelper / AWS profile,无需重启进程。

try/catch 包裹,错误 logError——onChange 不可 throw 否则 setState 链断裂。

源码引用: src/state/onChangeAppState.ts · 第 154–171 行(共 172 行)

 154|   // settings: clear auth-related caches when settings change
 155|   // This ensures apiKeyHelper and AWS/GCP credential changes take effect immediately
 156|   if (newState.settings !== oldState.settings) {
 157|     try {
 158|       clearApiKeyHelperCache()
 159|       clearAwsCredentialsCache()
 160|       clearGcpCredentialsCache()
 161| 
 162|       // Re-apply environment variables when settings.env changes
 163|       // This is additive-only: new vars are added, existing may be overwritten, nothing is deleted
 164|       if (newState.settings.env !== oldState.settings.env) {
 165|         applyConfigEnvironmentVariables()
 166|       }
 167|     } catch (error) {
 168|       logError(toError(error))
 169|     }
 170|   }
 171| }

externalMetadataToAppState 逆向映射

externalMetadataToAppState 供 remote worker / CCR control_request 把 SessionExternalMetadata 写回 AppState:

  • permission_mode string → toolPermissionContext.mode(permissionModeFromString)
  • is_ultraplan_mode boolean → isUltraplanMode

返回 (prev) =&gt; AppState updater,与 setState 直接兼容。

与 onChange 正向流配对:CCR 推 metadata → setAppState(externalMetadataToAppState(meta)) → 若 mode 变 → onChange 可能再次 notify(但 external 相等则 CCR notify 跳过)。

Worker restart 场景用此恢复 UI 与 harness 一致。

源码引用: src/state/onChangeAppState.ts · 第 23–41 行(共 172 行)

  23| // Inverse of the push below — restore on worker restart.
  24| export function externalMetadataToAppState(
  25|   metadata: SessionExternalMetadata,
  26| ): (prev: AppState) => AppState {
  27|   return prev => ({
  28|     ...prev,
  29|     ...(typeof metadata.permission_mode === 'string'
  30|       ? {
  31|           toolPermissionContext: {
  32|             ...prev.toolPermissionContext,
  33|             mode: permissionModeFromString(metadata.permission_mode),
  34|           },
  35|         }
  36|       : {}),
  37|     ...(typeof metadata.is_ultraplan_mode === 'boolean'
  38|       ? { isUltraplanMode: metadata.is_ultraplan_mode }
  39|       : {}),
  40|   })
  41| }

扩展 onChange 的约定

新增 AppState 字段需持久化或通知外部系统时的 checklist:

  1. 是否已有 scattered callsites?若有,迁入 onChange diff
  2. diff 用 !== 比较 primitive 或引用;nested mutate 不会触发
  3. 磁盘 IO 用 getGlobalConfig / saveGlobalConfig / updateSettingsForSource——避免在 selector 里写
  4. bootstrap 同步 import setMainLoopModelOverride 等 getter/setter,勿读 STATE 对象
  5. CCR/SDK 通知走 sessionState.ts 统一 API

selector 新增时保持纯函数,输入尽量 Pick&lt;AppState, ...&gt; 缩小测试 fixture。

本章小结与延伸

selectors = 纯派生;onChangeAppState = 持久化与 CCR/SDK 同步总线。下一章 teammate-state,读 transcript 视图状态机。 继续学习:

  • teammate-state
  • app-state-core
Prev
app-state-core · store、AppState 类型与 Provider
Next
teammate-state · 队友视图与 swarm 状态