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

本章总览

先看总流程:keybinding-registry 负责把“配置里的按键声明”变成“运行时可执行动作”,核心链路是解析、匹配、上下文过滤、handler 调用。对应文件分别承担 Context 契约、Provider 编排、resolver 纯函数和用户配置热加载。

学完本章你应该能

  • 描述 KeybindingProvider 的 value 字段职责
  • 说明 useKeybinding 的 switch(result.type) 分支
  • 理解 resolveKeyWithChordState 与 pendingChordRef
  • 知道 registerHandler 与 invokeAction 的分工
  • 解释 loadKeybindingsSyncWithWarnings 合并顺序
  • 能在 REPL 定位 KeybindingSetup 挂载点

核心概念(先读懂这些)

Handler 留在组件,Binding 来自配置

useKeybinding 注释强调 React 方式:handler 闭包在组件内,action 字符串从配置解析。多个组件可注册同一 action(不同 context),invokeAction 按 activeContexts 选第一个匹配 handler。

Chord 双通道:ref + state

pendingChordRef 供 resolver 同步读;pendingChord state 触发 UI( chord 提示)。CHORD_TIMEOUT_MS=1000 超时取消。ChordInterceptor 用 useInput 抢在所有 handler 之前,有意 disable prefer-use-keybindings lint。

GrowthBook 门控自定义

isKeybindingCustomizationEnabled() 查 tengu_keybinding_customization_release。未开启时 loadUserBindings 仍 parse 默认表,用户 JSON 可能被忽略或 Doctor 提示——与注释「仅 ANT 员工」历史文档需对照 GrowthBook 现状。

建议学习步骤

  1. 阅读 KeybindingContextValue 类型
  2. 跟踪 useKeybinding handleInput 分支
  3. 阅读 resolveKey 的 last-wins 循环
  4. 打开 KeybindingProviderSetup ChordInterceptor
  5. 阅读 loadUserBindings initializeKeybindingWatcher
  6. 对照 parser.ts parseBindings 输出 ParsedBinding

常见误区

注意

handler 返回 false 时不 stopPropagation

注意

unbound 动作会吞键——voice:pushToTalk 与 space 冲突注释在 defaultBindings

注意

无 KeybindingContext 时 useKeybinding 静默 no-op

注意

useKeybindings 减少多个 useInput 订阅,但共享同一 isActive

KeybindingContext API

KeybindingContextValue 暴露:

方法/字段作用
resolve(input, key, contexts)ChordResolveResult
setPendingChord更新 pending 序列
getDisplayText(action, context)快捷键展示 ctrl+t
bindings全量 ParsedBinding[]
activeContextsSet,子组件 register
registerHandleruseKeybinding 挂载
invokeActionChordInterceptor 回调

useOptionalKeybindingContext 无 Provider 返回 null——Doctor 等可选树不 crash。

源码引用: src/keybindings/KeybindingContext.tsx · 第 7–43 行(共 226 行)

   7| } from 'react'
   8| import type { Key } from '../ink.js'
   9| import {
  10|   type ChordResolveResult,
  11|   getBindingDisplayText,
  12|   resolveKeyWithChordState,
  13| } from './resolver.js'
  14| import type {
  15|   KeybindingContextName,
  16|   ParsedBinding,
  17|   ParsedKeystroke,
  18| } from './types.js'
  19| 
  20| /** Handler registration for action callbacks */
  21| type HandlerRegistration = {
  22|   action: string
  23|   context: KeybindingContextName
  24|   handler: () => void
  25| }
  26| 
  27| type KeybindingContextValue = {
  28|   /** Resolve a key input to an action name (with chord support) */
  29|   resolve: (
  30|     input: string,
  31|     key: Key,
  32|     activeContexts: KeybindingContextName[],
  33|   ) => ChordResolveResult
  34| 
  35|   /** Update the pending chord state */
  36|   setPendingChord: (pending: ParsedKeystroke[] | null) => void
  37| 
  38|   /** Get display text for an action (e.g., "ctrl+t") */
  39|   getDisplayText: (
  40|     action: string,
  41|     context: KeybindingContextName,
  42|   ) => string | undefined
  43| 

源码引用: src/keybindings/KeybindingContext.tsx · 第 134–160 行(共 226 行)

 134|       }
 135|       return false
 136|     }
 137| 
 138|     return {
 139|       // Use ref for immediate access to pending chord, avoiding React state delay
 140|       // This is critical for chord sequences where the second key might be pressed
 141|       // before React re-renders with the updated pendingChord state
 142|       resolve: (input, key, contexts) =>
 143|         resolveKeyWithChordState(
 144|           input,
 145|           key,
 146|           contexts,
 147|           bindings,
 148|           pendingChordRef.current,
 149|         ),
 150|       setPendingChord,
 151|       getDisplayText: getDisplay,
 152|       bindings,
 153|       pendingChord,
 154|       activeContexts,
 155|       registerActiveContext,
 156|       unregisterActiveContext,
 157|       registerHandler,
 158|       invokeAction,
 159|     }
 160|   }, [

useKeybinding 实现要点

useEffect 注册 handler 到 context;useInput 回调里:

  1. 组装 contextsToCheck = activeContexts + context + Global(去重保序)
  2. resolve → switch type
  3. match 且 action 相等 → handler() !== false 则 stopImmediatePropagation
  4. chord_started → setPendingChord + stop
  5. unbound → 清 chord + stop(显式禁用键)
  6. none → 放行

useKeybindings 批量版,减少 REPL 内 useInput 注册次数。

源码引用: src/keybindings/useKeybinding.ts · 第 33–97 行(共 197 行)

  33| export function useKeybinding(
  34|   action: string,
  35|   handler: () => void | false | Promise<void>,
  36|   options: Options = {},
  37| ): void {
  38|   const { context = 'Global', isActive = true } = options
  39|   const keybindingContext = useOptionalKeybindingContext()
  40| 
  41|   // Register handler with the context for ChordInterceptor to invoke
  42|   useEffect(() => {
  43|     if (!keybindingContext || !isActive) return
  44|     return keybindingContext.registerHandler({ action, context, handler })
  45|   }, [action, context, handler, keybindingContext, isActive])
  46| 
  47|   const handleInput = useCallback(
  48|     (input: string, key: Key, event: InputEvent) => {
  49|       // If no keybinding context available, skip resolution
  50|       if (!keybindingContext) return
  51| 
  52|       // Build context list: registered active contexts + this context + Global
  53|       // More specific contexts (registered ones) take precedence over Global
  54|       const contextsToCheck: KeybindingContextName[] = [
  55|         ...keybindingContext.activeContexts,
  56|         context,
  57|         'Global',
  58|       ]
  59|       // Deduplicate while preserving order (first occurrence wins for priority)
  60|       const uniqueContexts = [...new Set(contextsToCheck)]
  61| 
  62|       const result = keybindingContext.resolve(input, key, uniqueContexts)
  63| 
  64|       switch (result.type) {
  65|         case 'match':
  66|           // Chord completed (if any) - clear pending state
  67|           keybindingContext.setPendingChord(null)
  68|           if (result.action === action) {
  69|             if (handler() !== false) {
  70|               event.stopImmediatePropagation()
  71|             }
  72|           }
  73|           break
  74|         case 'chord_started':
  75|           // User started a chord sequence - update pending state
  76|           keybindingContext.setPendingChord(result.pending)
  77|           event.stopImmediatePropagation()
  78|           break
  79|         case 'chord_cancelled':
  80|           // Chord was cancelled (escape or invalid key)
  81|           keybindingContext.setPendingChord(null)
  82|           break
  83|         case 'unbound':
  84|           // Explicitly unbound - clear any pending chord
  85|           keybindingContext.setPendingChord(null)
  86|           event.stopImmediatePropagation()
  87|           break
  88|         case 'none':
  89|           // No match - let other handlers try
  90|           break
  91|       }
  92|     },
  93|     [action, context, handler, keybindingContext],
  94|   )
  95| 
  96|   useInput(handleInput, { isActive })
  97| }

源码引用: src/keybindings/useKeybinding.ts · 第 99–150 行(共 197 行)

  99| /**
 100|  * Handle multiple keybindings in one hook (reduces useInput calls).
 101|  *
 102|  * Supports chord sequences. When a chord is started, the hook will
 103|  * manage the pending state automatically.
 104|  *
 105|  * @example
 106|  * ```tsx
 107|  * useKeybindings({
 108|  *   'chat:submit': () =&gt; handleSubmit(),
 109|  *   'chat:cancel': () =&gt; handleCancel(),
 110|  * }, { context: 'Chat' })
 111|  * ```
 112|  */
 113| export function useKeybindings(
 114|   // Handler returning `false` means "not consumed" — the event propagates
 115|   // to later useInput/useKeybindings handlers. Useful for fall-through:
 116|   // e.g. ScrollKeybindingHandler's scroll:line* returns false when the
 117|   // ScrollBox content fits (scroll is a no-op), letting a child component's
 118|   // handler take the wheel event for list navigation instead. Promise<void>
 119|   // is allowed for fire-and-forget async handlers (the `!== false` check
 120|   // only skips propagation for a sync `false`, not a pending Promise).
 121|   handlers: Record<string, () => void | false | Promise<void>>,
 122|   options: Options = {},
 123| ): void {
 124|   const { context = 'Global', isActive = true } = options
 125|   const keybindingContext = useOptionalKeybindingContext()
 126| 
 127|   // Register all handlers with the context for ChordInterceptor to invoke
 128|   useEffect(() => {
 129|     if (!keybindingContext || !isActive) return
 130| 
 131|     const unregisterFns: Array<() => void> = []
 132|     for (const [action, handler] of Object.entries(handlers)) {
 133|       unregisterFns.push(
 134|         keybindingContext.registerHandler({ action, context, handler }),
 135|       )
 136|     }
 137| 
 138|     return () => {
 139|       for (const unregister of unregisterFns) {
 140|         unregister()
 141|       }
 142|     }
 143|   }, [context, handlers, keybindingContext, isActive])
 144| 
 145|   const handleInput = useCallback(
 146|     (input: string, key: Key, event: InputEvent) => {
 147|       // If no keybinding context available, skip resolution
 148|       if (!keybindingContext) return
 149| 
 150|       // Build context list: registered active contexts + this context + Global

resolver 纯函数

resolveKey:单键 binding,context 集合过滤,last match wins。

resolveKeyWithChordState:pending prefix + 下一键 → match | chord_started | chord_cancelled。

getBindingDisplayText:findLast 同 action+context,chordToString 格式化。

keystrokesEqual:alt/meta 合并——legacy 终端无法区分。

buildKeystroke:escape 时 effectiveMeta=false(Ink quirk)。

源码引用: src/keybindings/resolver.ts · 第 32–61 行(共 245 行)

  32| export function resolveKey(
  33|   input: string,
  34|   key: Key,
  35|   activeContexts: KeybindingContextName[],
  36|   bindings: ParsedBinding[],
  37| ): ResolveResult {
  38|   // Find matching bindings (last one wins for user overrides)
  39|   let match: ParsedBinding | undefined
  40|   const ctxSet = new Set(activeContexts)
  41| 
  42|   for (const binding of bindings) {
  43|     // Phase 1: Only single-keystroke bindings
  44|     if (binding.chord.length !== 1) continue
  45|     if (!ctxSet.has(binding.context)) continue
  46| 
  47|     if (matchesBinding(input, key, binding)) {
  48|       match = binding
  49|     }
  50|   }
  51| 
  52|   if (!match) {
  53|     return { type: 'none' }
  54|   }
  55| 
  56|   if (match.action === null) {
  57|     return { type: 'unbound' }
  58|   }
  59| 
  60|   return { type: 'match', action: match.action }
  61| }

源码引用: src/keybindings/resolver.ts · 第 82–118 行(共 245 行)

  82| function buildKeystroke(input: string, key: Key): ParsedKeystroke | null {
  83|   const keyName = getKeyName(input, key)
  84|   if (!keyName) return null
  85| 
  86|   // QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts).
  87|   // This is legacy terminal behavior - we should NOT record this as a modifier
  88|   // for the escape key itself, otherwise chord matching will fail.
  89|   const effectiveMeta = key.escape ? false : key.meta
  90| 
  91|   return {
  92|     key: keyName,
  93|     ctrl: key.ctrl,
  94|     alt: effectiveMeta,
  95|     shift: key.shift,
  96|     meta: effectiveMeta,
  97|     super: key.super,
  98|   }
  99| }
 100| 
 101| /**
 102|  * Compare two ParsedKeystrokes for equality. Collapses alt/meta into
 103|  * one logical modifier — legacy terminals can't distinguish them (see
 104|  * match.ts modifiersMatch), so "alt+k" and "meta+k" are the same key.
 105|  * Super (cmd/win) is distinct — only arrives via kitty keyboard protocol.
 106|  */
 107| export function keystrokesEqual(
 108|   a: ParsedKeystroke,
 109|   b: ParsedKeystroke,
 110| ): boolean {
 111|   return (
 112|     a.key === b.key &&
 113|     a.ctrl === b.ctrl &&
 114|     a.shift === b.shift &&
 115|     (a.alt || a.meta) === (b.alt || b.meta) &&
 116|     a.super === b.super
 117|   )
 118| }

源码引用: src/keybindings/match.ts · 第 1–50 行(共 121 行)

   1| import type { Key } from '../ink.js'
   2| import type { ParsedBinding, ParsedKeystroke } from './types.js'
   3| 
   4| /**
   5|  * Modifier keys from Ink's Key type that we care about for matching.
   6|  * Note: `fn` from Key is intentionally excluded as it's rarely used and
   7|  * not commonly configurable in terminal applications.
   8|  */
   9| type InkModifiers = Pick<Key, 'ctrl' | 'shift' | 'meta' | 'super'>
  10| 
  11| /**
  12|  * Extract modifiers from an Ink Key object.
  13|  * This function ensures we're explicitly extracting the modifiers we care about.
  14|  */
  15| function getInkModifiers(key: Key): InkModifiers {
  16|   return {
  17|     ctrl: key.ctrl,
  18|     shift: key.shift,
  19|     meta: key.meta,
  20|     super: key.super,
  21|   }
  22| }
  23| 
  24| /**
  25|  * Extract the normalized key name from Ink's Key + input.
  26|  * Maps Ink's boolean flags (key.escape, key.return, etc.) to string names
  27|  * that match our ParsedKeystroke.key format.
  28|  */
  29| export function getKeyName(input: string, key: Key): string | null {
  30|   if (key.escape) return 'escape'
  31|   if (key.return) return 'enter'
  32|   if (key.tab) return 'tab'
  33|   if (key.backspace) return 'backspace'
  34|   if (key.delete) return 'delete'
  35|   if (key.upArrow) return 'up'
  36|   if (key.downArrow) return 'down'
  37|   if (key.leftArrow) return 'left'
  38|   if (key.rightArrow) return 'right'
  39|   if (key.pageUp) return 'pageup'
  40|   if (key.pageDown) return 'pagedown'
  41|   if (key.wheelUp) return 'wheelup'
  42|   if (key.wheelDown) return 'wheeldown'
  43|   if (key.home) return 'home'
  44|   if (key.end) return 'end'
  45|   if (input.length === 1) return input.toLowerCase()
  46|   return null
  47| }
  48| 
  49| /**
  50|  * Check if all modifiers match between Ink Key and ParsedKeystroke.

KeybindingProviderSetup

KeybindingSetup 职责:

  • initializeKeybindingWatcher + subscribeToKeybindingChanges
  • loadKeybindingsSyncWithWarnings 初始 load
  • pendingChordRef + useState pendingChord
  • handlerRegistryRef: Map<action, Set<HandlerRegistration>>
  • ChordInterceptor:全局 useInput,chord 未完成时吞键
  • useKeybindingWarnings → notifications

REPL.tsx 结构:AppStateProvider → KeybindingSetup → GlobalKeybindingHandlers + CommandKeybindingHandlers。

源码引用: src/keybindings/KeybindingProviderSetup.tsx · 第 26–54 行(共 382 行)

  26| import { resolveKeyWithChordState } from './resolver.js'
  27| import type {
  28|   KeybindingContextName,
  29|   ParsedBinding,
  30|   ParsedKeystroke,
  31| } from './types.js'
  32| import type { KeybindingWarning } from './validate.js'
  33| 
  34| /**
  35|  * Timeout for chord sequences in milliseconds.
  36|  * If the user doesn't complete the chord within this time, it's cancelled.
  37|  */
  38| const CHORD_TIMEOUT_MS = 1000
  39| 
  40| type Props = {
  41|   children: React.ReactNode
  42| }
  43| 
  44| /**
  45|  * Keybinding provider with default + user bindings and hot-reload support.
  46|  *
  47|  * Usage: Wrap your app with this provider to enable keybinding support.
  48|  *
  49|  * ```tsx
  50|  * &lt;AppStateProvider&gt;
  51|  *   &lt;KeybindingSetup&gt;
  52|  *     &lt;REPL ... /&gt;
  53|  *   &lt;/KeybindingSetup&gt;
  54|  * &lt;/AppStateProvider&gt;

源码引用: src/keybindings/KeybindingProviderSetup.tsx · 第 100–180 行(共 382 行)

 100|       // Keep visible for 60 seconds like settings errors
 101|       timeoutMs: 60000,
 102|     })
 103|   }, [warnings, isReload, addNotification, removeNotification])
 104| }
 105| 
 106| export function KeybindingSetup({ children }: Props): React.ReactNode {
 107|   // Load bindings synchronously for initial render
 108|   const [{ bindings, warnings }, setLoadResult] =
 109|     useState&lt;KeybindingsLoadResult&gt;(() =&gt; {
 110|       const result = loadKeybindingsSyncWithWarnings()
 111|       logForDebugging(
 112|         `[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`,
 113|       )
 114|       return result
 115|     })
 116| 
 117|   // Track if this is a reload (not initial load)
 118|   const [isReload, setIsReload] = useState(false)
 119| 
 120|   // Display warnings via notifications
 121|   useKeybindingWarnings(warnings, isReload)
 122| 
 123|   // Chord state management - use ref for immediate access, state for re-renders
 124|   // The ref is used by resolve() to get the current value without waiting for re-render
 125|   // The state is used to trigger re-renders when needed (e.g., for UI updates)
 126|   const pendingChordRef = useRef&lt;ParsedKeystroke[] | null&gt;(null)
 127|   const [pendingChord, setPendingChordState] = useState&lt;
 128|     ParsedKeystroke[] | null
 129|   &gt;(null)
 130|   const chordTimeoutRef = useRef&lt;NodeJS.Timeout | null&gt;(null)
 131| 
 132|   // Handler registry for action callbacks (used by ChordInterceptor to invoke handlers)
 133|   const handlerRegistryRef = useRef(
 134|     new Map&lt;
 135|       string,
 136|       Set&lt;{
 137|         action: string
 138|         context: KeybindingContextName
 139|         handler: () =&gt; void
 140|       }&gt;
 141|     &gt;(),
 142|   )
 143| 
 144|   // Active context tracking for keybinding priority resolution
 145|   // Using a ref instead of state for synchronous updates - input handlers need
 146|   // to see the current value immediately, not after a React render cycle.
 147|   const activeContextsRef = useRef&lt;Set&lt;KeybindingContextName&gt;&gt;(new Set())
 148| 
 149|   const registerActiveContext = useCallback(
 150|     (context: KeybindingContextName) =&gt; {
 151|       activeContextsRef.current.add(context)
 152|     },
 153|     [],
 154|   )
 155| 
 156|   const unregisterActiveContext = useCallback(
 157|     (context: KeybindingContextName) =&gt; {
 158|       activeContextsRef.current.delete(context)
 159|     },
 160|     [],
 161|   )
 162| 
 163|   // Clear chord timeout when component unmounts or chord changes
 164|   const clearChordTimeout = useCallback(() =&gt; {
 165|     if (chordTimeoutRef.current) {
 166|       clearTimeout(chordTimeoutRef.current)
 167|       chordTimeoutRef.current = null
 168|     }
 169|   }, [])
 170| 
 171|   // Wrapper for setPendingChord that manages timeout and syncs ref+state
 172|   const setPendingChord = useCallback(
 173|     (pending: ParsedKeystroke[] | null) =&gt; {
 174|       clearChordTimeout()
 175| 
 176|       if (pending !== null) {
 177|         // Set timeout to cancel chord if not completed
 178|         chordTimeoutRef.current = setTimeout(
 179|           (pendingChordRef, setPendingChordState) =&gt; {
 180|             logForDebugging('[keybindings] Chord timeout - cancelling')

loadUserBindings 热加载

KeybindingsLoadResult = { bindings, warnings }。

流程:读 keybindings.json → jsonParse → validateBindings + checkDuplicateKeysInJson → parseBindings 合并 DEFAULT_BINDINGS 之后。

chokidar 监听 FILE_STABILITY_THRESHOLD_MS=500 防抖。Telemetry:custom bindings 每日最多 log 一次。

parseBindings(parser.ts)把 KeybindingBlock[] 转为 ParsedBinding[],支持 null action 表示 unbind。

源码引用: src/keybindings/loadUserBindings.ts · 第 41–64 行(共 473 行)

  41| export function isKeybindingCustomizationEnabled(): boolean {
  42|   return getFeatureValue_CACHED_MAY_BE_STALE(
  43|     'tengu_keybinding_customization_release',
  44|     false,
  45|   )
  46| }
  47| 
  48| /**
  49|  * Time in milliseconds to wait for file writes to stabilize.
  50|  */
  51| const FILE_STABILITY_THRESHOLD_MS = 500
  52| 
  53| /**
  54|  * Polling interval for checking file stability.
  55|  */
  56| const FILE_STABILITY_POLL_INTERVAL_MS = 200
  57| 
  58| /**
  59|  * Result of loading keybindings, including any validation warnings.
  60|  */
  61| export type KeybindingsLoadResult = {
  62|   bindings: ParsedBinding[]
  63|   warnings: KeybindingWarning[]
  64| }

源码引用: src/keybindings/loadUserBindings.ts · 第 100–180 行(共 473 行)

 100|     typeof b.bindings === 'object' &&
 101|     b.bindings !== null
 102|   )
 103| }
 104| 
 105| /**
 106|  * Type guard to check if an array contains only valid KeybindingBlocks.
 107|  */
 108| function isKeybindingBlockArray(arr: unknown): arr is KeybindingBlock[] {
 109|   return Array.isArray(arr) && arr.every(isKeybindingBlock)
 110| }
 111| 
 112| /**
 113|  * Get the path to the user keybindings file.
 114|  */
 115| export function getKeybindingsPath(): string {
 116|   return join(getClaudeConfigHomeDir(), 'keybindings.json')
 117| }
 118| 
 119| /**
 120|  * Parse default bindings (cached for performance).
 121|  */
 122| function getDefaultParsedBindings(): ParsedBinding[] {
 123|   return parseBindings(DEFAULT_BINDINGS)
 124| }
 125| 
 126| /**
 127|  * Load and parse keybindings from user config file.
 128|  * Returns merged default + user bindings along with validation warnings.
 129|  *
 130|  * For external users, always returns default bindings only.
 131|  * User customization is currently gated to Anthropic employees.
 132|  */
 133| export async function loadKeybindings(): Promise&lt;KeybindingsLoadResult&gt; {
 134|   const defaultBindings = getDefaultParsedBindings()
 135| 
 136|   // Skip user config loading for external users
 137|   if (!isKeybindingCustomizationEnabled()) {
 138|     return { bindings: defaultBindings, warnings: [] }
 139|   }
 140| 
 141|   const userPath = getKeybindingsPath()
 142| 
 143|   try {
 144|     const content = await readFile(userPath, 'utf-8')
 145|     const parsed: unknown = jsonParse(content)
 146| 
 147|     // Extract bindings array from object wrapper format: { "bindings": [...] }
 148|     let userBlocks: unknown
 149|     if (typeof parsed === 'object' && parsed !== null && 'bindings' in parsed) {
 150|       userBlocks = (parsed as { bindings: unknown }).bindings
 151|     } else {
 152|       // Invalid format - missing bindings property
 153|       const errorMessage = 'keybindings.json must have a "bindings" array'
 154|       const suggestion = 'Use format: { "bindings": [ ... ] }'
 155|       logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
 156|       return {
 157|         bindings: defaultBindings,
 158|         warnings: [
 159|           {
 160|             type: 'parse_error',
 161|             severity: 'error',
 162|             message: errorMessage,
 163|             suggestion,
 164|           },
 165|         ],
 166|       }
 167|     }
 168| 
 169|     // Validate structure - bindings must be an array of valid keybinding blocks
 170|     if (!isKeybindingBlockArray(userBlocks)) {
 171|       const errorMessage = !Array.isArray(userBlocks)
 172|         ? '"bindings" must be an array'
 173|         : 'keybindings.json contains invalid block structure'
 174|       const suggestion = !Array.isArray(userBlocks)
 175|         ? 'Set "bindings" to an array of keybinding blocks'
 176|         : 'Each block must have "context" (string) and "bindings" (object)'
 177|       logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
 178|       return {
 179|         bindings: defaultBindings,
 180|         warnings: [

源码引用: src/keybindings/parser.ts · 第 1–60 行(共 204 行)

   1| import type {
   2|   Chord,
   3|   KeybindingBlock,
   4|   ParsedBinding,
   5|   ParsedKeystroke,
   6| } from './types.js'
   7| 
   8| /**
   9|  * Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke.
  10|  * Supports various modifier aliases (ctrl/control, alt/opt/option/meta,
  11|  * cmd/command/super/win).
  12|  */
  13| export function parseKeystroke(input: string): ParsedKeystroke {
  14|   const parts = input.split('+')
  15|   const keystroke: ParsedKeystroke = {
  16|     key: '',
  17|     ctrl: false,
  18|     alt: false,
  19|     shift: false,
  20|     meta: false,
  21|     super: false,
  22|   }
  23|   for (const part of parts) {
  24|     const lower = part.toLowerCase()
  25|     switch (lower) {
  26|       case 'ctrl':
  27|       case 'control':
  28|         keystroke.ctrl = true
  29|         break
  30|       case 'alt':
  31|       case 'opt':
  32|       case 'option':
  33|         keystroke.alt = true
  34|         break
  35|       case 'shift':
  36|         keystroke.shift = true
  37|         break
  38|       case 'meta':
  39|         keystroke.meta = true
  40|         break
  41|       case 'cmd':
  42|       case 'command':
  43|       case 'super':
  44|       case 'win':
  45|         keystroke.super = true
  46|         break
  47|       case 'esc':
  48|         keystroke.key = 'escape'
  49|         break
  50|       case 'return':
  51|         keystroke.key = 'enter'
  52|         break
  53|       case 'space':
  54|         keystroke.key = ' '
  55|         break
  56|       case '↑':
  57|         keystroke.key = 'up'
  58|         break
  59|       case '↓':
  60|         keystroke.key = 'down'

validate 与 schema

validate.ts 产出 KeybindingWarning:reserved key 冲突、未知 action、duplicate chord。

reservedShortcuts.ts 禁止用户覆盖 ctrl+c、ctrl+d 等。

schema.ts + template.ts 生成默认 JSON 模板供 /doctor 或文档引用。

注册链路调试:/doctor keybindings 段 → warnings 列表 → 对照 bindings 数组 find action。

源码引用: src/keybindings/validate.ts · 第 1–60 行(共 499 行)

   1| import { plural } from '../utils/stringUtils.js'
   2| import { chordToString, parseChord, parseKeystroke } from './parser.js'
   3| import {
   4|   getReservedShortcuts,
   5|   normalizeKeyForComparison,
   6| } from './reservedShortcuts.js'
   7| import type {
   8|   KeybindingBlock,
   9|   KeybindingContextName,
  10|   ParsedBinding,
  11| } from './types.js'
  12| 
  13| /**
  14|  * Types of validation issues that can occur with keybindings.
  15|  */
  16| export type KeybindingWarningType =
  17|   | 'parse_error'
  18|   | 'duplicate'
  19|   | 'reserved'
  20|   | 'invalid_context'
  21|   | 'invalid_action'
  22| 
  23| /**
  24|  * A warning or error about a keybinding configuration issue.
  25|  */
  26| export type KeybindingWarning = {
  27|   type: KeybindingWarningType
  28|   severity: 'error' | 'warning'
  29|   message: string
  30|   key?: string
  31|   context?: string
  32|   action?: string
  33|   suggestion?: string
  34| }
  35| 
  36| /**
  37|  * Type guard to check if an object is a valid KeybindingBlock.
  38|  */
  39| function isKeybindingBlock(obj: unknown): obj is KeybindingBlock {
  40|   if (typeof obj !== 'object' || obj === null) return false
  41|   const b = obj as Record&lt;string, unknown&gt;
  42|   return (
  43|     typeof b.context === 'string' &&
  44|     typeof b.bindings === 'object' &&
  45|     b.bindings !== null
  46|   )
  47| }
  48| 
  49| /**
  50|  * Type guard to check if an array contains only valid KeybindingBlocks.
  51|  */
  52| function isKeybindingBlockArray(arr: unknown): arr is KeybindingBlock[] {
  53|   return Array.isArray(arr) && arr.every(isKeybindingBlock)
  54| }
  55| 
  56| /**
  57|  * Valid context names for keybindings.
  58|  * Must match KeybindingContextName in types.ts
  59|  */
  60| const VALID_CONTEXTS: KeybindingContextName[] = [

源码引用: src/keybindings/reservedShortcuts.ts · 第 1–40 行(共 128 行)

   1| import { getPlatform } from '../utils/platform.js'
   2| 
   3| /**
   4|  * Shortcuts that are typically intercepted by the OS, terminal, or shell
   5|  * and will likely never reach the application.
   6|  */
   7| export type ReservedShortcut = {
   8|   key: string
   9|   reason: string
  10|   severity: 'error' | 'warning'
  11| }
  12| 
  13| /**
  14|  * Shortcuts that cannot be rebound - they are hardcoded in Claude Code.
  15|  */
  16| export const NON_REBINDABLE: ReservedShortcut[] = [
  17|   {
  18|     key: 'ctrl+c',
  19|     reason: 'Cannot be rebound - used for interrupt/exit (hardcoded)',
  20|     severity: 'error',
  21|   },
  22|   {
  23|     key: 'ctrl+d',
  24|     reason: 'Cannot be rebound - used for exit (hardcoded)',
  25|     severity: 'error',
  26|   },
  27|   {
  28|     key: 'ctrl+m',
  29|     reason:
  30|       'Cannot be rebound - identical to Enter in terminals (both send CR)',
  31|     severity: 'error',
  32|   },
  33| ]
  34| 
  35| /**
  36|  * Terminal control shortcuts that are intercepted by the terminal/OS.
  37|  * These will likely never reach the application.
  38|  *
  39|  * Note: ctrl+s (XOFF) and ctrl+q (XON) are NOT included here because:
  40|  * - Most modern terminals disable flow control by default

registerActiveContext 模式

Settings Dialog、Transcript 搜索等子树在 mount 时 registerActiveContext('Settings'),unmount unregister。useKeybinding 的 context 参数声明「本 handler 所属 context」,resolver 要求 binding.context 落在 activeContexts 并集内。

Transcript 打开时 activeContexts 含 Transcript,Global 的 ctrl+o 可能与 Transcript 专用 binding 共存——具体以 defaultBindings Transcript 块为准。

为什么用 ref 管同步键盘状态

KeybindingProviderSetup 对 pendingChord、handlerRegistry、activeContexts 都偏向 ref,是因为终端键盘事件需要在同一个 useInput 回调内立刻读到最新状态。pendingChordRef 让 resolver 判断“上一键是否已经启动 chord”,pendingChord state 只负责让 UI 重渲染显示提示;activeContextsRef 让 Dialog 刚 mount 后的第一个按键就进入正确 context;handlerRegistryRef 让 ChordInterceptor 能在子组件 handler 注册后直接 invoke action,而不用等待 React state 批处理完成。

ChordInterceptor 的位置也很重要:它渲染在 provider 内、children 前,注释明确它必须先于 PromptInput 捕获 chord 序列。否则 ctrl+x ctrl+e 这类两段键第二段可能被文本输入框当普通字符吃掉。调试快捷键时,应先看 resolveKeyWithChordState 的结果是否是 match/chord_started/unbound,再看 registry 中是否有 action handler,最后看 handler 所属 context 是否已 active。这个顺序能区分“配置没有解析到 action”“action 解析了但没有 handler”“handler 存在但 context 不生效”三类问题。

Provider 还承担热加载隔离。loadUserBindings 重新读文件后只替换 bindings 与 warnings,组件注册的 handler 闭包仍留在 registry;这意味着用户改键位不需要重新 mount PromptInput 或 Dialog。相反,如果某个功能没有调用 useKeybinding 或 useKeybindings 注册 action,即使用户 JSON 里写了合法 action,resolver 也只能产生命中结果,最终不会执行任何业务逻辑。因此 registry 章节要和 default-bindings、command-bindings 一起读,才能看清配置、解析、注册、调用四步。

源码引用: src/keybindings/KeybindingProviderSetup.tsx · 第 138–187 行(共 382 行)

 138|         context: KeybindingContextName
 139|         handler: () =&gt; void
 140|       }&gt;
 141|     &gt;(),
 142|   )
 143| 
 144|   // Active context tracking for keybinding priority resolution
 145|   // Using a ref instead of state for synchronous updates - input handlers need
 146|   // to see the current value immediately, not after a React render cycle.
 147|   const activeContextsRef = useRef&lt;Set&lt;KeybindingContextName&gt;&gt;(new Set())
 148| 
 149|   const registerActiveContext = useCallback(
 150|     (context: KeybindingContextName) =&gt; {
 151|       activeContextsRef.current.add(context)
 152|     },
 153|     [],
 154|   )
 155| 
 156|   const unregisterActiveContext = useCallback(
 157|     (context: KeybindingContextName) =&gt; {
 158|       activeContextsRef.current.delete(context)
 159|     },
 160|     [],
 161|   )
 162| 
 163|   // Clear chord timeout when component unmounts or chord changes
 164|   const clearChordTimeout = useCallback(() =&gt; {
 165|     if (chordTimeoutRef.current) {
 166|       clearTimeout(chordTimeoutRef.current)
 167|       chordTimeoutRef.current = null
 168|     }
 169|   }, [])
 170| 
 171|   // Wrapper for setPendingChord that manages timeout and syncs ref+state
 172|   const setPendingChord = useCallback(
 173|     (pending: ParsedKeystroke[] | null) =&gt; {
 174|       clearChordTimeout()
 175| 
 176|       if (pending !== null) {
 177|         // Set timeout to cancel chord if not completed
 178|         chordTimeoutRef.current = setTimeout(
 179|           (pendingChordRef, setPendingChordState) =&gt; {
 180|             logForDebugging('[keybindings] Chord timeout - cancelling')
 181|             pendingChordRef.current = null
 182|             setPendingChordState(null)
 183|           },
 184|           CHORD_TIMEOUT_MS,
 185|           pendingChordRef,
 186|           setPendingChordState,
 187|         )

源码引用: src/keybindings/KeybindingProviderSetup.tsx · 第 211–253 行(共 382 行)

 211|       )
 212|     })
 213| 
 214|     return () =&gt; {
 215|       unsubscribe()
 216|       clearChordTimeout()
 217|     }
 218|   }, [clearChordTimeout])
 219| 
 220|   return (
 221|     &lt;KeybindingProvider
 222|       bindings={bindings}
 223|       pendingChordRef={pendingChordRef}
 224|       pendingChord={pendingChord}
 225|       setPendingChord={setPendingChord}
 226|       activeContexts={activeContextsRef.current}
 227|       registerActiveContext={registerActiveContext}
 228|       unregisterActiveContext={unregisterActiveContext}
 229|       handlerRegistryRef={handlerRegistryRef}
 230|     &gt;
 231|       &lt;ChordInterceptor
 232|         bindings={bindings}
 233|         pendingChordRef={pendingChordRef}
 234|         setPendingChord={setPendingChord}
 235|         activeContexts={activeContextsRef.current}
 236|         handlerRegistryRef={handlerRegistryRef}
 237|       /&gt;
 238|       {children}
 239|     &lt;/KeybindingProvider&gt;
 240|   )
 241| }
 242| 
 243| /**
 244|  * Global chord interceptor that registers useInput FIRST (before children).
 245|  *
 246|  * This component intercepts keystrokes that are part of chord sequences and
 247|  * stops propagation before other handlers (like PromptInput) can see them.
 248|  *
 249|  * Without this, the second key of a chord (e.g., 'r' in "ctrl+c r") would be
 250|  * captured by PromptInput and added to the input field before the keybinding
 251|  * system could recognize it as completing a chord.
 252|  */
 253| type HandlerRegistration = {

源码引用: src/keybindings/KeybindingContext.tsx · 第 13–43 行(共 226 行)

  13| } from './resolver.js'
  14| import type {
  15|   KeybindingContextName,
  16|   ParsedBinding,
  17|   ParsedKeystroke,
  18| } from './types.js'
  19| 
  20| /** Handler registration for action callbacks */
  21| type HandlerRegistration = {
  22|   action: string
  23|   context: KeybindingContextName
  24|   handler: () =&gt; void
  25| }
  26| 
  27| type KeybindingContextValue = {
  28|   /** Resolve a key input to an action name (with chord support) */
  29|   resolve: (
  30|     input: string,
  31|     key: Key,
  32|     activeContexts: KeybindingContextName[],
  33|   ) =&gt; ChordResolveResult
  34| 
  35|   /** Update the pending chord state */
  36|   setPendingChord: (pending: ParsedKeystroke[] | null) =&gt; void
  37| 
  38|   /** Get display text for an action (e.g., "ctrl+t") */
  39|   getDisplayText: (
  40|     action: string,
  41|     context: KeybindingContextName,
  42|   ) =&gt; string | undefined
  43| 

排障最短路径:配置到执行四跳

遇到“按键没反应”时,优先按四跳排障:第一跳看 loadUserBindings 是否把 JSON 解析成 ParsedBinding;第二跳看 resolver 是否在当前 contexts 下得到 match/chord_started;第三跳看 handlerRegistryRef 是否注册了该 action;第四跳看 handler 返回值是否阻止了继续传播。这个顺序能快速定位是配置问题、匹配问题、注册问题还是业务处理问题。

实践里最常见误区是直接怀疑 useInput 顺序,结果忽略了 action 根本没注册。因为 registry 与 bindings 是解耦的:bindings 只定义“应该触发哪个 action”,是否执行要看对应功能是否调用 useKeybinding/useKeybindings 注册过 handler。

源码引用: src/keybindings/loadUserBindings.ts · 第 100–180 行(共 473 行)

 100|     typeof b.bindings === 'object' &&
 101|     b.bindings !== null
 102|   )
 103| }
 104| 
 105| /**
 106|  * Type guard to check if an array contains only valid KeybindingBlocks.
 107|  */
 108| function isKeybindingBlockArray(arr: unknown): arr is KeybindingBlock[] {
 109|   return Array.isArray(arr) && arr.every(isKeybindingBlock)
 110| }
 111| 
 112| /**
 113|  * Get the path to the user keybindings file.
 114|  */
 115| export function getKeybindingsPath(): string {
 116|   return join(getClaudeConfigHomeDir(), 'keybindings.json')
 117| }
 118| 
 119| /**
 120|  * Parse default bindings (cached for performance).
 121|  */
 122| function getDefaultParsedBindings(): ParsedBinding[] {
 123|   return parseBindings(DEFAULT_BINDINGS)
 124| }
 125| 
 126| /**
 127|  * Load and parse keybindings from user config file.
 128|  * Returns merged default + user bindings along with validation warnings.
 129|  *
 130|  * For external users, always returns default bindings only.
 131|  * User customization is currently gated to Anthropic employees.
 132|  */
 133| export async function loadKeybindings(): Promise&lt;KeybindingsLoadResult&gt; {
 134|   const defaultBindings = getDefaultParsedBindings()
 135| 
 136|   // Skip user config loading for external users
 137|   if (!isKeybindingCustomizationEnabled()) {
 138|     return { bindings: defaultBindings, warnings: [] }
 139|   }
 140| 
 141|   const userPath = getKeybindingsPath()
 142| 
 143|   try {
 144|     const content = await readFile(userPath, 'utf-8')
 145|     const parsed: unknown = jsonParse(content)
 146| 
 147|     // Extract bindings array from object wrapper format: { "bindings": [...] }
 148|     let userBlocks: unknown
 149|     if (typeof parsed === 'object' && parsed !== null && 'bindings' in parsed) {
 150|       userBlocks = (parsed as { bindings: unknown }).bindings
 151|     } else {
 152|       // Invalid format - missing bindings property
 153|       const errorMessage = 'keybindings.json must have a "bindings" array'
 154|       const suggestion = 'Use format: { "bindings": [ ... ] }'
 155|       logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
 156|       return {
 157|         bindings: defaultBindings,
 158|         warnings: [
 159|           {
 160|             type: 'parse_error',
 161|             severity: 'error',
 162|             message: errorMessage,
 163|             suggestion,
 164|           },
 165|         ],
 166|       }
 167|     }
 168| 
 169|     // Validate structure - bindings must be an array of valid keybinding blocks
 170|     if (!isKeybindingBlockArray(userBlocks)) {
 171|       const errorMessage = !Array.isArray(userBlocks)
 172|         ? '"bindings" must be an array'
 173|         : 'keybindings.json contains invalid block structure'
 174|       const suggestion = !Array.isArray(userBlocks)
 175|         ? 'Set "bindings" to an array of keybinding blocks'
 176|         : 'Each block must have "context" (string) and "bindings" (object)'
 177|       logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
 178|       return {
 179|         bindings: defaultBindings,
 180|         warnings: [

源码引用: src/keybindings/resolver.ts · 第 32–61 行(共 245 行)

  32| export function resolveKey(
  33|   input: string,
  34|   key: Key,
  35|   activeContexts: KeybindingContextName[],
  36|   bindings: ParsedBinding[],
  37| ): ResolveResult {
  38|   // Find matching bindings (last one wins for user overrides)
  39|   let match: ParsedBinding | undefined
  40|   const ctxSet = new Set(activeContexts)
  41| 
  42|   for (const binding of bindings) {
  43|     // Phase 1: Only single-keystroke bindings
  44|     if (binding.chord.length !== 1) continue
  45|     if (!ctxSet.has(binding.context)) continue
  46| 
  47|     if (matchesBinding(input, key, binding)) {
  48|       match = binding
  49|     }
  50|   }
  51| 
  52|   if (!match) {
  53|     return { type: 'none' }
  54|   }
  55| 
  56|   if (match.action === null) {
  57|     return { type: 'unbound' }
  58|   }
  59| 
  60|   return { type: 'match', action: match.action }
  61| }

源码引用: src/keybindings/KeybindingProviderSetup.tsx · 第 145–187 行(共 382 行)

 145|   // Using a ref instead of state for synchronous updates - input handlers need
 146|   // to see the current value immediately, not after a React render cycle.
 147|   const activeContextsRef = useRef&lt;Set&lt;KeybindingContextName&gt;&gt;(new Set())
 148| 
 149|   const registerActiveContext = useCallback(
 150|     (context: KeybindingContextName) =&gt; {
 151|       activeContextsRef.current.add(context)
 152|     },
 153|     [],
 154|   )
 155| 
 156|   const unregisterActiveContext = useCallback(
 157|     (context: KeybindingContextName) =&gt; {
 158|       activeContextsRef.current.delete(context)
 159|     },
 160|     [],
 161|   )
 162| 
 163|   // Clear chord timeout when component unmounts or chord changes
 164|   const clearChordTimeout = useCallback(() =&gt; {
 165|     if (chordTimeoutRef.current) {
 166|       clearTimeout(chordTimeoutRef.current)
 167|       chordTimeoutRef.current = null
 168|     }
 169|   }, [])
 170| 
 171|   // Wrapper for setPendingChord that manages timeout and syncs ref+state
 172|   const setPendingChord = useCallback(
 173|     (pending: ParsedKeystroke[] | null) =&gt; {
 174|       clearChordTimeout()
 175| 
 176|       if (pending !== null) {
 177|         // Set timeout to cancel chord if not completed
 178|         chordTimeoutRef.current = setTimeout(
 179|           (pendingChordRef, setPendingChordState) =&gt; {
 180|             logForDebugging('[keybindings] Chord timeout - cancelling')
 181|             pendingChordRef.current = null
 182|             setPendingChordState(null)
 183|           },
 184|           CHORD_TIMEOUT_MS,
 185|           pendingChordRef,
 186|           setPendingChordState,
 187|         )

本章小结与延伸

keybinding-registry = Provider + resolver + Hook 三角。下一章 default-bindings 读 DEFAULT_BINDINGS 全貌。 继续学习:

  • default-bindings
  • keybindings 总览
Prev
模块: keybindings
Next
default-bindings · 默认键位表与平台差异