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

本章总览

Claude Code 启动时自带内置工具/命令/MCP 客户端,运行中 MCP 子系统还会动态发现新资源。合并态 Hook 负责在 React 层把「props 初始集」与「运行时 MCP 状态」合成 REPL 可用的单一视图,并保证与 headless runAgent 路径一致。四条 Hook 都很薄——真正业务逻辑在 assembleToolPool、mergeAndFilterTools、mergeClients 与 processQueueIfReady 等纯函数里;读本章时要同时打开 utils/ 与 tools.ts。

学完本章你应该能

  • 解释 useMergedTools 为何 delegate 到 assembleToolPool + mergeAndFilterTools
  • 说明 initialTools 在 dedup 时优先于 assembled 的含义
  • 理解 useMergedCommands / useMergedClients 的 uniqBy 合并策略
  • 画出 useQueueProcessor 与 QueryGuard、messageQueueManager 的协作
  • 列举 processQueueIfReady 对 slash 命令与 batch 命令的不同处理
  • 能在 REPL.tsx 找到 mergedTools 注入 query 循环的位置

核心概念(先读懂这些)

React Hook 只是 memo 壳

useMergedTools / useMergedCommands / useMergedClients 本质都是 useMemo 包裹的纯合并函数。这样 REPL 仅在依赖变化时重算工具池,而 runAgent、print.ts 可直接 import 同名的纯函数(mergeAndFilterTools、mergeClients)避免拉入 react/ink。改合并规则时,优先改 utils/toolPool.ts 或 export 的 mergeClients,再让 Hook 透传。

prompt-cache 稳定的排序

assembleToolPool 与 mergeAndFilterTools 都对 built-in 与 MCP 工具 分区排序:built-in 必须保持 contiguous prefix,因为服务端 claude_code_system_cache_policy 在最后一个 prefix-matched built-in 后打 cache breakpoint。若 flat sort 把 MCP 工具插进 built-in 中间,任意 MCP 增删都会 invalidate 下游 cache key——这是性能级约束,不是 cosmetic。

useSyncExternalStore 绕过 Ink 通知延迟

useQueueProcessor 用 useSyncExternalStore 订阅 QueryGuard 与 command queue module store,注释写明:React context 传播在 Ink 下可能丢通知,导致队列有项却不处理。这是 REPL 能在 turn 结束后可靠 drain 队列的关键。

建议学习步骤

  1. 阅读 tools.ts assembleToolPool 注释与实现
  2. 对照 utils/toolPool.ts mergeAndFilterTools 的 partition + coordinator filter
  3. 看 useMergedTools useMemo 依赖数组
  4. 读 useMergedCommands / mergeClients 的 uniqBy 逻辑
  5. 打开 useQueueProcessor 的 effect 条件与 processQueueIfReady 调用
  6. 在 REPL.tsx 搜索 useMergedTools 与 useQueueProcessor

常见误区

注意

useMergedTools 依赖数组含 replBridgeEnabled 占位变量——变更时确保不会意外 stale

注意

useQueueProcessor 不在 effect 里 reserve QueryGuard—— reservation 在 handlePromptSubmit 同步链完成

注意

队列里 subagent 寻址项会被 processQueueIfReady 跳过,否则会永久 stall 主线程队列

注意

Coordinator 模式下 mergeAndFilterTools 会 applyCoordinatorToolFilter,与 main.tsx headless 路径必须同步

在 REPL 数据流中的位置

REPL 初始化与运行期数据流:

props / bootstrap
  → combinedInitialTools + mcp.tools + mcp.commands + mcp.clients
       ↓
useMergedTools / useMergedCommands / useMergedClients
       ↓
query 循环、slash 命令、权限 UI、compact
       ↓
用户输入 / 任务通知 → messageQueueManager.enqueue
       ↓
useQueueProcessor(query 空闲且无 local JSX UI)
       ↓
processQueueIfReady → executeQueuedInput → handlePromptSubmit

useCanUseTool 消费 mergedTools 里的 permission 上下文;compact 注释写明 context.options.tools 含 MCP merge 结果。任何「工具列表不对」的 bug,应从此处四 Hook 向上追 props,向下追 assembleToolPool。

useMergedTools:双层合并

useMergedTools(initialTools, mcpTools, toolPermissionContext) 在 useMemo 内:

  1. 调用 assembleToolPool(toolPermissionContext, mcpTools) — 内置 getTools + MCP deny 过滤 + 按名 dedup(内置优先)
  2. 调用 mergeAndFilterTools(initialTools, assembled, mode) — 把 props 带来的 extra tools 叠加上去,再 coordinator 过滤

注释强调 initialTools 可能已含 getTools() 结果,与 assembled 重叠;uniqBy name 去重,initialTools 排在前面故优先。

Hook 本身约 40 行,是 REPL 与 runAgent 共享语义 的 React 适配层。headless main.tsx 直接调 mergeAndFilterTools,不走 Hook。

源码引用: src/hooks/useMergedTools.ts · 第 8–44 行(共 45 行)

   8| /**
   9|  * React hook that assembles the full tool pool for the REPL.
  10|  *
  11|  * Uses assembleToolPool() (the shared pure function used by both REPL and runAgent)
  12|  * to combine built-in tools with MCP tools, applying deny rules and deduplication.
  13|  * Any extra initialTools are merged on top.
  14|  *
  15|  * @param initialTools - Extra tools to include (built-in + startup MCP from props).
  16|  *   These are merged with the assembled pool and take precedence in deduplication.
  17|  * @param mcpTools - MCP tools discovered dynamically (from mcp state)
  18|  * @param toolPermissionContext - Permission context for filtering
  19|  */
  20| export function useMergedTools(
  21|   initialTools: Tools,
  22|   mcpTools: Tools,
  23|   toolPermissionContext: ToolPermissionContext,
  24| ): Tools {
  25|   let replBridgeEnabled = false
  26|   let replBridgeOutboundOnly = false
  27|   return useMemo(() => {
  28|     // assembleToolPool is the shared function that both REPL and runAgent use.
  29|     // It handles: getTools() + MCP deny-rule filtering + dedup + MCP CLI exclusion.
  30|     const assembled = assembleToolPool(toolPermissionContext, mcpTools)
  31| 
  32|     return mergeAndFilterTools(
  33|       initialTools,
  34|       assembled,
  35|       toolPermissionContext.mode,
  36|     )
  37|   }, [
  38|     initialTools,
  39|     mcpTools,
  40|     toolPermissionContext,
  41|     replBridgeEnabled,
  42|     replBridgeOutboundOnly,
  43|   ])
  44| }

源码引用: src/tools.ts · 第 329–367 行(共 390 行)

 329| /**
 330|  * Assemble the full tool pool for a given permission context and MCP tools.
 331|  *
 332|  * This is the single source of truth for combining built-in tools with MCP tools.
 333|  * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
 334|  * use this function to ensure consistent tool pool assembly.
 335|  *
 336|  * The function:
 337|  * 1. Gets built-in tools via getTools() (respects mode filtering)
 338|  * 2. Filters MCP tools by deny rules
 339|  * 3. Deduplicates by tool name (built-in tools take precedence)
 340|  *
 341|  * @param permissionContext - Permission context for filtering built-in tools
 342|  * @param mcpTools - MCP tools from appState.mcp.tools
 343|  * @returns Combined, deduplicated array of built-in and MCP tools
 344|  */
 345| export function assembleToolPool(
 346|   permissionContext: ToolPermissionContext,
 347|   mcpTools: Tools,
 348| ): Tools {
 349|   const builtInTools = getTools(permissionContext)
 350| 
 351|   // Filter out MCP tools that are in the deny list
 352|   const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
 353| 
 354|   // Sort each partition for prompt-cache stability, keeping built-ins as a
 355|   // contiguous prefix. The server's claude_code_system_cache_policy places a
 356|   // global cache breakpoint after the last prefix-matched built-in tool; a flat
 357|   // sort would interleave MCP tools into built-ins and invalidate all downstream
 358|   // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
 359|   // preserves insertion order, so built-ins win on name conflict.
 360|   // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
 361|   // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
 362|   const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
 363|   return uniqBy(
 364|     [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
 365|     'name',
 366|   )
 367| }

mergeAndFilterTools:排序与 Coordinator 过滤

utils/toolPool.ts 中的 mergeAndFilterTools 是 React-free 纯函数,print.ts 也可 import。

步骤:

  1. uniqBy([...initialTools, ...assembled], 'name') — initial 优先
  2. partition(..., isMcpTool) — 拆 MCP 与 built-in
  3. 各自 sort by name,再 [...builtIn, ...mcp] 拼回(built-in prefix)
  4. 若 COORDINATOR_MODE 且 isCoordinatorMode(),走 applyCoordinatorToolFilter

applyCoordinatorToolFilter 只允许 COORDINATOR_MODE_ALLOWED_TOOLS 集合内的工具,外加 PR activity subscription 后缀工具(subscribe_pr_activity 等),因为 coordinator 直接 orchestrate PR 订阅而非 delegate worker。

工程含义: 新增 MCP 工具默认对 coordinator 不可见,除非加入 allowlist 或符合 suffix 规则。

源码引用: src/utils/toolPool.ts · 第 16–41 行(共 80 行)

  16| export function isPrActivitySubscriptionTool(name: string): boolean {
  17|   return PR_ACTIVITY_TOOL_SUFFIXES.some(suffix => name.endsWith(suffix))
  18| }
  19| 
  20| // Dead code elimination: conditional imports for feature-gated modules
  21| /* eslint-disable @typescript-eslint/no-require-imports */
  22| const coordinatorModeModule = feature('COORDINATOR_MODE')
  23|   ? (require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js'))
  24|   : null
  25| /* eslint-enable @typescript-eslint/no-require-imports */
  26| 
  27| /**
  28|  * Filters a tool array to the set allowed in coordinator mode.
  29|  * Shared between the REPL path (mergeAndFilterTools) and the headless
  30|  * path (main.tsx) so both stay in sync.
  31|  *
  32|  * PR activity subscription tools are always allowed since subscription
  33|  * management is orchestration.
  34|  */
  35| export function applyCoordinatorToolFilter(tools: Tools): Tools {
  36|   return tools.filter(
  37|     t =>
  38|       COORDINATOR_MODE_ALLOWED_TOOLS.has(t.name) ||
  39|       isPrActivitySubscriptionTool(t.name),
  40|   )
  41| }

源码引用: src/utils/toolPool.ts · 第 43–79 行(共 80 行)

  43| /**
  44|  * Pure function that merges tool pools and applies coordinator mode filtering.
  45|  *
  46|  * Lives in a React-free file so print.ts can import it without pulling
  47|  * react/ink into the SDK module graph. The useMergedTools hook delegates
  48|  * to this function inside useMemo.
  49|  *
  50|  * @param initialTools - Extra tools to include (built-in + startup MCP from props).
  51|  * @param assembled - Tools from assembleToolPool (built-in + MCP, deduped).
  52|  * @param mode - The permission context mode.
  53|  * @returns Merged, deduplicated, and coordinator-filtered tool array.
  54|  */
  55| export function mergeAndFilterTools(
  56|   initialTools: Tools,
  57|   assembled: Tools,
  58|   mode: ToolPermissionContext['mode'],
  59| ): Tools {
  60|   // Merge initialTools on top - they take precedence in deduplication.
  61|   // initialTools may include built-in tools (from getTools() in REPL.tsx) which
  62|   // overlap with assembled tools. uniqBy handles this deduplication.
  63|   // Partition-sort for prompt-cache stability (same as assembleToolPool):
  64|   // built-ins must stay a contiguous prefix for the server's cache policy.
  65|   const [mcp, builtIn] = partition(
  66|     uniqBy([...initialTools, ...assembled], 'name'),
  67|     isMcpTool,
  68|   )
  69|   const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  70|   const tools = [...builtIn.sort(byName), ...mcp.sort(byName)]
  71| 
  72|   if (feature('COORDINATOR_MODE') && coordinatorModeModule) {
  73|     if (coordinatorModeModule.isCoordinatorMode()) {
  74|       return applyCoordinatorToolFilter(tools)
  75|     }
  76|   }
  77| 
  78|   return tools
  79| }

useMergedCommands:斜杠命令合并

useMergedCommands(initialCommands, mcpCommands) 逻辑极简:

  • mcpCommands.length > 0 时:uniqBy([...initialCommands, ...mcpCommands], 'name')
  • 否则直接返回 initialCommands

MCP 服务器可注册 commands(与 tools 不同),例如远程定义的 slash 扩展。uniqBy name 意味着同名命令 MCP 不会覆盖内置——数组顺序决定 precedence,与 tools 一致(initial 在前)。

REPL 把合并结果传给 slash 命令解析器与自动补全。若用户反映「MCP 命令不出现」,先查 mcp.commands 状态是否为空,再查 name 是否与内置冲突被 dedup 掉。

源码引用: src/hooks/useMergedCommands.ts · 第 1–15 行(共 16 行)

   1| import uniqBy from 'lodash-es/uniqBy.js'
   2| import { useMemo } from 'react'
   3| import type { Command } from '../commands.js'
   4| 
   5| export function useMergedCommands(
   6|   initialCommands: Command[],
   7|   mcpCommands: Command[],
   8| ): Command[] {
   9|   return useMemo(() => {
  10|     if (mcpCommands.length > 0) {
  11|       return uniqBy([...initialCommands, ...mcpCommands], 'name')
  12|     }
  13|     return initialCommands
  14|   }, [initialCommands, mcpCommands])
  15| }

useMergedClients 与 mergeClients

MCP 连接状态(failed / needs-auth / connected)在 UI 横幅、/mcp 面板中展示,需要合并 bootstrap clients 与运行时发现的 clients。

mergeClients(initial, mcp):
  if initial && mcp?.length > 0 → uniqBy([...initial, ...mcp], 'name')
  else → initial || []

mergeClients 导出供测试与非 React 代码复用;useMergedClients 仅 useMemo 包装。name 作为唯一键——同名 server 以 initial 列表优先。

关联: useMcpConnectivityStatus(notifs 章)消费 merged clients 数组,按 type/config.type 分桶发通知。

源码引用: src/hooks/useMergedClients.ts · 第 5–23 行(共 24 行)

   5| export function mergeClients(
   6|   initialClients: MCPServerConnection[] | undefined,
   7|   mcpClients: readonly MCPServerConnection[] | undefined,
   8| ): MCPServerConnection[] {
   9|   if (initialClients && mcpClients && mcpClients.length > 0) {
  10|     return uniqBy([...initialClients, ...mcpClients], 'name')
  11|   }
  12|   return initialClients || []
  13| }
  14| 
  15| export function useMergedClients(
  16|   initialClients: MCPServerConnection[] | undefined,
  17|   mcpClients: MCPServerConnection[] | undefined,
  18| ): MCPServerConnection[] {
  19|   return useMemo(
  20|     () => mergeClients(initialClients, mcpClients),
  21|     [initialClients, mcpClients],
  22|   )
  23| }

useQueueProcessor:何时 drain 队列

useQueueProcessor 接收:

参数作用
executeQueuedInput批量/单条执行队列命令(通常接 REPL executeUserInput)
hasActiveLocalJsxUIlocal JSX 命令占用输入时为 true,阻塞处理
queryGuardQueryGuard 实例,subscribe/getSnapshot 供 useSyncExternalStore

effect 条件(全部满足才 process):

  1. !isQueryActive — 无进行中的 query turn
  2. !hasActiveLocalJsxUI
  3. queueSnapshot.length > 0

满足时调用 processQueueIfReady({ executeInput: executeQueuedInput })。

注释解释 reservation 已移至 handlePromptSubmit 同步链:dequeue 后 executeQueuedInput → queryGuard.reserve() 在首个 await 前完成,effect 重入时 isQueryActive 已为 true,避免双 drain。

源码引用: src/hooks/useQueueProcessor.ts · 第 10–67 行(共 69 行)

  10| type UseQueueProcessorParams = {
  11|   executeQueuedInput: (commands: QueuedCommand[]) => Promise<void>
  12|   hasActiveLocalJsxUI: boolean
  13|   queryGuard: QueryGuard
  14| }
  15| 
  16| /**
  17|  * Hook that processes queued commands when conditions are met.
  18|  *
  19|  * Uses a single unified command queue (module-level store). Priority determines
  20|  * processing order: 'now' > 'next' (user input) > 'later' (task notifications).
  21|  * The dequeue() function handles priority ordering automatically.
  22|  *
  23|  * Processing triggers when:
  24|  * - No query active (queryGuard — reactive via useSyncExternalStore)
  25|  * - Queue has items
  26|  * - No active local JSX UI blocking input
  27|  */
  28| export function useQueueProcessor({
  29|   executeQueuedInput,
  30|   hasActiveLocalJsxUI,
  31|   queryGuard,
  32| }: UseQueueProcessorParams): void {
  33|   // Subscribe to the query guard. Re-renders when a query starts or ends
  34|   // (or when reserve/cancelReservation transitions dispatching state).
  35|   const isQueryActive = useSyncExternalStore(
  36|     queryGuard.subscribe,
  37|     queryGuard.getSnapshot,
  38|   )
  39| 
  40|   // Subscribe to the unified command queue via useSyncExternalStore.
  41|   // This guarantees re-render when the store changes, bypassing
  42|   // React context propagation delays that cause missed notifications in Ink.
  43|   const queueSnapshot = useSyncExternalStore(
  44|     subscribeToCommandQueue,
  45|     getCommandQueueSnapshot,
  46|   )
  47| 
  48|   useEffect(() => {
  49|     if (isQueryActive) return
  50|     if (hasActiveLocalJsxUI) return
  51|     if (queueSnapshot.length === 0) return
  52| 
  53|     // Reservation is now owned by handlePromptSubmit (inside executeUserInput's
  54|     // try block). The sync chain executeQueuedInput → handlePromptSubmit →
  55|     // executeUserInput → queryGuard.reserve() runs before the first real await,
  56|     // so by the time React re-runs this effect (due to the dequeue-triggered
  57|     // snapshot change), isQueryActive is already true (dispatching) and the
  58|     // guard above returns early. handlePromptSubmit's finally releases the
  59|     // reservation via cancelReservation() (no-op if onQuery already ran end()).
  60|     processQueueIfReady({ executeInput: executeQueuedInput })
  61|   }, [
  62|     queueSnapshot,
  63|     isQueryActive,
  64|     executeQueuedInput,
  65|     hasActiveLocalJsxUI,
  66|     queryGuard,
  67|   ])

processQueueIfReady:优先级与 slash 单条处理

utils/queueProcessor.ts 实现队列 出队策略:

  • 统一队列由 messageQueueManager 管理,dequeue() 按 priority 排序:now > next(用户输入)> later(任务通知)
  • slash 命令(value 以 / 开头)与 bash-mode 逐条 process,保证独立错误隔离与 progress UI
  • 其他同 mode 的非 slash 项可 batch drain,作为 QueuedCommand[] 一次传入 executeInput
  • 跳过 subagent 寻址项,防止主线程 peek 到 subagent 通知后 processed:false 永久卡死

caller 负责每轮 command 完成后再次触发 processor 直到队列为空——useQueueProcessor 的 effect 依赖 queueSnapshot 变化自动重入。

query.ts 注释:某些 slash 应在 turn 结束后由 useQueueProcessor 触发,而非 mid-turn 插队。

源码引用: src/utils/queueProcessor.ts · 第 17–60 行(共 96 行)

  17| /**
  18|  * Check if a queued command is a slash command (value starts with '/').
  19|  */
  20| function isSlashCommand(cmd: QueuedCommand): boolean {
  21|   if (typeof cmd.value === 'string') {
  22|     return cmd.value.trim().startsWith('/')
  23|   }
  24|   // For ContentBlockParam[], check the first text block
  25|   for (const block of cmd.value) {
  26|     if (block.type === 'text') {
  27|       return block.text.trim().startsWith('/')
  28|     }
  29|   }
  30|   return false
  31| }
  32| 
  33| /**
  34|  * Processes commands from the queue.
  35|  *
  36|  * Slash commands (starting with '/') and bash-mode commands are processed
  37|  * one at a time so each goes through the executeInput path individually.
  38|  * Bash commands need individual processing to preserve per-command error
  39|  * isolation, exit codes, and progress UI. Other non-slash commands are
  40|  * batched: all items **with the same mode** as the highest-priority item
  41|  * are drained at once and passed as a single array to executeInput — each
  42|  * becomes its own user message with its own UUID. Different modes
  43|  * (e.g. prompt vs task-notification) are never mixed because they are
  44|  * treated differently downstream.
  45|  *
  46|  * The caller is responsible for ensuring no query is currently running
  47|  * and for calling this function again after each command completes
  48|  * until the queue is empty.
  49|  *
  50|  * @returns result with processed status
  51|  */
  52| export function processQueueIfReady({
  53|   executeInput,
  54| }: ProcessQueueParams): ProcessQueueResult {
  55|   // This processor runs on the REPL main thread between turns. Skip anything
  56|   // addressed to a subagent — an unfiltered peek() returning a subagent
  57|   // notification would set targetMode, dequeueAllMatching would find nothing
  58|   // matching that mode with agentId===undefined, and we'd return processed:
  59|   // false with the queue unchanged → the React effect never re-fires and any
  60|   // queued user prompt stalls permanently.

REPL 挂载点与 headless 对齐

REPL.tsx 典型调用:

const mergedTools = useMergedTools(combinedInitialTools, mcp.tools, toolPermissionContext);
useQueueProcessor({ executeQueuedInput, hasActiveLocalJsxUI, queryGuard });

combinedInitialTools 汇集 props.tools 与内置 getTools();mcp.tools 来自 AppState MCP 子状态。

main.tsx runAgent / coordinator worker 路径注释「mirrors useMergedTools filtering」——改 filter 时必须两边验证。cli/print.ts 亦提到 TUI useQueueProcessor 行为应对齐。

compact.ts 使用 permission-filtered tools 来源与 useMergedTools 相同,避免 compact 后模型看到的工具集与 REPL 不一致。

源码引用: src/screens/REPL.tsx · 第 811–811 行(共 7050 行)

 811|         </Text>

源码引用: src/tools.ts · 第 289–334 行(共 390 行)

 289|     // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
 290|     // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
 291|     if (
 292|       feature('COORDINATOR_MODE') &&
 293|       coordinatorModeModule?.isCoordinatorMode()
 294|     ) {
 295|       simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
 296|     }
 297|     return filterToolsByDenyRules(simpleTools, permissionContext)
 298|   }
 299| 
 300|   // Get all base tools and filter out special tools that get added conditionally
 301|   const specialTools = new Set([
 302|     ListMcpResourcesTool.name,
 303|     ReadMcpResourceTool.name,
 304|     SYNTHETIC_OUTPUT_TOOL_NAME,
 305|   ])
 306| 
 307|   const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
 308| 
 309|   // Filter out tools that are denied by the deny rules
 310|   let allowedTools = filterToolsByDenyRules(tools, permissionContext)
 311| 
 312|   // When REPL mode is enabled, hide primitive tools from direct use.
 313|   // They're still accessible inside REPL via the VM context.
 314|   if (isReplModeEnabled()) {
 315|     const replEnabled = allowedTools.some(tool =>
 316|       toolMatchesName(tool, REPL_TOOL_NAME),
 317|     )
 318|     if (replEnabled) {
 319|       allowedTools = allowedTools.filter(
 320|         tool => !REPL_ONLY_TOOLS.has(tool.name),
 321|       )
 322|     }
 323|   }
 324| 
 325|   const isEnabled = allowedTools.map(_ => _.isEnabled())
 326|   return allowedTools.filter((_, i) => isEnabled[i])
 327| }
 328| 
 329| /**
 330|  * Assemble the full tool pool for a given permission context and MCP tools.
 331|  *
 332|  * This is the single source of truth for combining built-in tools with MCP tools.
 333|  * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
 334|  * use this function to ensure consistent tool pool assembly.

源码目录(本主题相关文件)

强关联:utils/toolPool.ts、utils/queueProcessor.ts、utils/messageQueueManager.js、utils/QueryGuard.js、tools.ts(assembleToolPool)。

动手练习

  1. 在 REPL 连接一个 MCP server,观察 mergedTools 长度变化及 built-in 是否仍为 prefix
  2. 队列中连续 enqueue 两条 slash 命令,确认逐条执行而非 batch
  3. 阅读 COORDINATOR_MODE_ALLOWED_TOOLS,列出 coordinator 可见而 worker 不可见的工具差异
  4. 模拟 query 进行中 enqueue,确认 useQueueProcessor 不 drain;turn 结束后自动 drain
  5. 对照 mergeClients 与 /mcp UI,理解 failed vs needs-auth 通知分桶

本章小结与延伸

合并态 Hook = 把静态配置与动态 MCP/队列状态合成 REPL 单一真相源。下一章建议 notifs,理解横幅如何提示 MCP 连接失败等合并结果。 继续学习:

  • hooks 模块总览
  • notifs 通知
Prev
输入与快捷键 Hook
Next
notifs 通知 Hook