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

本章总览

先看本质:command-bindings 是一层动态桥接,把 keybindings 里的 command:* action 转译成真实 slash 命令提交。它不维护命令实现本身,只负责发现绑定、门控触发场景,并以 fromKeybinding 语义把命令送入统一提交管线。

学完本章你应该能

  • 解释 commandActions 如何从 bindings 提取
  • 说明 fromKeybinding 选项对 handlePromptSubmit 的影响
  • 理解 isActive && !isModalOverlayActive 门控
  • 知道 NOOP_HELPERS 为何 setCursorOffset/clearBuffer 为空
  • 能在 keybindings.json 添加自定义 command:* 示例
  • 区分 command:* 与 prompt 命令 registry 名称

核心概念(先读懂这些)

动态发现而非静态表

不像 app:toggleTodos 在 useGlobalKeybindings 硬编码,command:* handler 完全由用户配置驱动。新增 /foo 命令无需改 keybindings 模块——用户写 command:foo 即可,只要 command 名与 registry 一致。

immediate 语义

注释强调:keybinding 触发 treated as immediate,preserve existing input text。与普通 Enter 提交不同,用户可能正在写长 prompt 时按快捷键执行 /compact,完成后 buffer 仍在。

Chat context 专属

useKeybindings(handlers, { context: Chat, isActive })——Global 键不会误触发 slash 命令。Modal overlay 激活时 isActive false,避免与 Dialog 抢键。

建议学习步骤

  1. 阅读 CommandKeybindingHandlers 文件头注释
  2. 跟踪 commandActions Set 构建循环
  3. 阅读 handlers map 与 onSubmit 调用
  4. 在 handlePromptSubmit 搜索 fromKeybinding
  5. 查看 schema.ts 是否文档化 command: 前缀
  6. 在 REPL.tsx 搜索 CommandKeybindingHandlers 挂载

常见误区

注意

command 名大小写需与 registry name 一致

注意

local-jsx 命令(如 /memory)从 keybinding 触发仍走同一 onSubmit 路径

注意

无 keybindingContext 时 commandActions 为空 Set

注意

command:unknown 会走 submit 但可能显示 unknown command

CommandKeybindingHandlers 流程

keybindingContext.bindings
  → filter action?.startsWith('command:')
  → commandActions Set
  → for each: map[action] = () => onSubmit('/'+name, NOOP_HELPERS, undefined, { fromKeybinding: true })
  → useKeybindings(map, { context: 'Chat', isActive })

组件零 UI,必须渲染在 KeybindingSetup 内。onSubmit 类型接受 rest 参数以兼容 speculationAccept 等扩展位。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 1–36 行(共 83 行)

   1| /**
   2|  * Component that registers keybinding handlers for command bindings.
   3|  *
   4|  * Must be rendered inside KeybindingSetup to have access to the keybinding context.
   5|  * Reads "command:*" actions from the current keybinding configuration and registers
   6|  * handlers that invoke the corresponding slash command via onSubmit.
   7|  *
   8|  * Commands triggered via keybinding are treated as "immediate" - they execute right
   9|  * away and preserve the user's existing input text (the prompt is not cleared).
  10|  */
  11| import { useMemo } from 'react'
  12| import { useIsModalOverlayActive } from '../context/overlayContext.js'
  13| import { useOptionalKeybindingContext } from '../keybindings/KeybindingContext.js'
  14| import { useKeybindings } from '../keybindings/useKeybinding.js'
  15| import type { PromptInputHelpers } from '../utils/handlePromptSubmit.js'
  16| 
  17| type Props = {
  18|   // onSubmit accepts additional parameters beyond what we pass here,
  19|   // so we use a rest parameter to allow any additional args
  20|   onSubmit: (
  21|     input: string,
  22|     helpers: PromptInputHelpers,
  23|     ...rest: [
  24|       speculationAccept?: undefined,
  25|       options?: { fromKeybinding?: boolean },
  26|     ]
  27|   ) => void
  28|   /** Set to false to disable command keybindings (e.g., when a dialog is open) */
  29|   isActive?: boolean
  30| }
  31| 
  32| const NOOP_HELPERS: PromptInputHelpers = {
  33|   setCursorOffset: () => {},
  34|   clearBuffer: () => {},
  35|   resetHistory: () => {},
  36| }

源码引用: src/hooks/useCommandKeybindings.tsx · 第 37–83 行(共 83 行)

  37| 
  38| /**
  39|  * Registers keybinding handlers for all "command:*" actions found in the
  40|  * user's keybinding configuration. When triggered, each handler submits
  41|  * the corresponding slash command (e.g., "command:commit" submits "/commit").
  42|  */
  43| export function CommandKeybindingHandlers({
  44|   onSubmit,
  45|   isActive = true,
  46| }: Props): null {
  47|   const keybindingContext = useOptionalKeybindingContext()
  48|   const isModalOverlayActive = useIsModalOverlayActive()
  49| 
  50|   // Extract command actions from parsed bindings
  51|   const commandActions = useMemo(() => {
  52|     if (!keybindingContext) return new Set<string>()
  53|     const actions = new Set<string>()
  54|     for (const binding of keybindingContext.bindings) {
  55|       if (binding.action?.startsWith('command:')) {
  56|         actions.add(binding.action)
  57|       }
  58|     }
  59|     return actions
  60|   }, [keybindingContext])
  61| 
  62|   // Build handler map for all command actions
  63|   const handlers = useMemo(() => {
  64|     const map: Record<string, () => void> = {}
  65|     for (const action of commandActions) {
  66|       const commandName = action.slice('command:'.length)
  67|       map[action] = () => {
  68|         onSubmit(`/${commandName}`, NOOP_HELPERS, undefined, {
  69|           fromKeybinding: true,
  70|         })
  71|       }
  72|     }
  73|     return map
  74|   }, [commandActions, onSubmit])
  75| 
  76|   useKeybindings(handlers, {
  77|     context: 'Chat',
  78|     isActive: isActive && !isModalOverlayActive,
  79|   })
  80| 
  81|   return null
  82| }
  83| 

NOOP_HELPERS 设计

PromptInputHelpers 正常提供 setCursorOffset、clearBuffer、resetHistory。Command keybinding 路径传入 no-op 函数——因为 immediate 模式 不应 清空或移动光标。

若未来某 command 需要 manip buffer,应显式扩展 fromKeybinding 分支而非默认 clearBuffer。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 26–30 行(共 83 行)

  26|     ]
  27|   ) => void
  28|   /** Set to false to disable command keybindings (e.g., when a dialog is open) */
  29|   isActive?: boolean
  30| }

Modal overlay 门控

useIsModalOverlayActive 来自 overlayContext。当 /memory、权限 Dialog 等 modal 打开时,command keybindings 暂停,防止 ctrl+ 绑定的 /commit 在 Dialog 内误触。

GlobalKeybindingHandlers 有类似 isActive 模式;Command 层额外检查 modal 因 Chat context 在 modal 下 technically 仍 active。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 93–83 行(共 83 行)


源码引用: src/context/overlayContext.tsx · 第 1–40 行(共 110 行)

   1| /**
   2|  * Overlay tracking for Escape key coordination.
   3|  *
   4|  * This solves the problem of escape key handling when overlays (like Select with onCancel)
   5|  * are open. The CancelRequestHandler needs to know when an overlay is active so it doesn't
   6|  * cancel requests when the user just wants to dismiss the overlay.
   7|  *
   8|  * Usage:
   9|  * 1. Call useRegisterOverlay() in any overlay component to automatically register it
  10|  * 2. Call useIsOverlayActive() to check if any overlay is currently active
  11|  *
  12|  * The hook automatically registers on mount and unregisters on unmount,
  13|  * so no manual cleanup or state management is needed.
  14|  */
  15| import { useContext, useEffect, useLayoutEffect } from 'react'
  16| import instances from '../ink/instances.js'
  17| import { AppStoreContext, useAppState } from '../state/AppState.js'
  18| 
  19| // Non-modal overlays that shouldn't disable TextInput focus
  20| const NON_MODAL_OVERLAYS = new Set(['autocomplete'])
  21| 
  22| /**
  23|  * Hook to register a component as an active overlay.
  24|  * Automatically registers on mount and unregisters on unmount.
  25|  *
  26|  * @param id - Unique identifier for this overlay (e.g., 'select', 'multi-select')
  27|  * @param enabled - Whether to register (default: true). Use this to conditionally register
  28|  *                  based on component props, e.g., only register when onCancel is provided.
  29|  *
  30|  * @example
  31|  * // Conditional registration based on whether cancel is supported
  32|  * function useSelectInput({ state }) {
  33|  *   useRegisterOverlay('select', !!state.onCancel)
  34|  *   // ...
  35|  * }
  36|  */
  37| export function useRegisterOverlay(id: string, enabled = true): void {
  38|   // Use context directly so this is a no-op when rendered outside AppStateProvider
  39|   // (e.g., in isolated component tests that don't need the full app state tree).
  40|   const store = useContext(AppStoreContext)

handlePromptSubmit 与 fromKeybinding

handlePromptSubmit(utils/handlePromptSubmit.ts)解析 leading slash,查 commands registry,区分 prompt/local/local-jsx。

options.fromKeybinding 标记来源 analytics(tengu_keybinding_command_invoke)并可能跳过某些 readline 前置逻辑。immediate flag 确保不等待 prompt 清空再 dispatch。

与 CommandKeybindingHandlers 配合:键 → slash+命令名 字符串 → 与普通 slash 相同 pipeline。

源码引用: src/utils/handlePromptSubmit.ts · 第 1–60 行(共 611 行)

   1| import type { UUID } from 'crypto'
   2| import { logEvent } from 'src/services/analytics/index.js'
   3| import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js'
   4| import { type Command, getCommandName, isCommandEnabled } from '../commands.js'
   5| import { selectableUserMessagesFilter } from '../components/MessageSelector.js'
   6| import type { SpinnerMode } from '../components/Spinner/types.js'
   7| import type { QuerySource } from '../constants/querySource.js'
   8| import { expandPastedTextRefs, parseReferences } from '../history.js'
   9| import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
  10| import type { IDESelection } from '../hooks/useIdeSelection.js'
  11| import type { AppState } from '../state/AppState.js'
  12| import type { SetToolJSXFn } from '../Tool.js'
  13| import type { LocalJSXCommandOnDone } from '../types/command.js'
  14| import type { Message } from '../types/message.js'
  15| import {
  16|   isValidImagePaste,
  17|   type PromptInputMode,
  18|   type QueuedCommand,
  19| } from '../types/textInputTypes.js'
  20| import { createAbortController } from './abortController.js'
  21| import type { PastedContent } from './config.js'
  22| import { logForDebugging } from './debug.js'
  23| import type { EffortValue } from './effort.js'
  24| import type { FileHistoryState } from './fileHistory.js'
  25| import { fileHistoryEnabled, fileHistoryMakeSnapshot } from './fileHistory.js'
  26| import { gracefulShutdownSync } from './gracefulShutdown.js'
  27| import { enqueue } from './messageQueueManager.js'
  28| import { resolveSkillModelOverride } from './model/model.js'
  29| import type { ProcessUserInputContext } from './processUserInput/processUserInput.js'
  30| import { processUserInput } from './processUserInput/processUserInput.js'
  31| import type { QueryGuard } from './QueryGuard.js'
  32| import { queryCheckpoint, startQueryProfile } from './queryProfiler.js'
  33| import { runWithWorkload } from './workloadContext.js'
  34| 
  35| function exit(): void {
  36|   gracefulShutdownSync(0)
  37| }
  38| 
  39| type BaseExecutionParams = {
  40|   queuedCommands?: QueuedCommand[]
  41|   messages: Message[]
  42|   mainLoopModel: string
  43|   ideSelection: IDESelection | undefined
  44|   querySource: QuerySource
  45|   commands: Command[]
  46|   queryGuard: QueryGuard
  47|   /**
  48|    * True when external loading (remote session, foregrounded background task)
  49|    * is active. These don't route through queryGuard, so the queue check must
  50|    * account for them separately. Omit (defaults to false) for the dequeue path
  51|    * (executeQueuedInput) — dequeued items were already queued past this check.
  52|    */
  53|   isExternalLoading?: boolean
  54|   setToolJSX: SetToolJSXFn
  55|   getToolUseContext: (
  56|     messages: Message[],
  57|     newMessages: Message[],
  58|     abortController: AbortController,
  59|     mainLoopModel: string,
  60|   ) => ProcessUserInputContext

源码引用: src/utils/handlePromptSubmit.ts · 第 100–160 行(共 611 行)

 100|     React.SetStateAction<Record<number, PastedContent>>
 101|   >
 102|   abortController?: AbortController | null
 103|   addNotification?: (notification: {
 104|     key: string
 105|     text: string
 106|     priority: 'low' | 'medium' | 'high' | 'immediate'
 107|   }) => void
 108|   setMessages?: (updater: (prev: Message[]) => Message[]) => void
 109|   streamMode?: SpinnerMode
 110|   hasInterruptibleToolInProgress?: boolean
 111|   uuid?: UUID
 112|   /**
 113|    * When true, input starting with `/` is treated as plain text.
 114|    * Used for remotely-received messages (bridge/CCR) that should not
 115|    * trigger local slash commands or skills.
 116|    */
 117|   skipSlashCommands?: boolean
 118| }
 119| 
 120| export async function handlePromptSubmit(
 121|   params: HandlePromptSubmitParams,
 122| ): Promise<void> {
 123|   const {
 124|     helpers,
 125|     queryGuard,
 126|     isExternalLoading = false,
 127|     commands,
 128|     onInputChange,
 129|     setPastedContents,
 130|     setToolJSX,
 131|     getToolUseContext,
 132|     messages,
 133|     mainLoopModel,
 134|     ideSelection,
 135|     setUserInputOnProcessing,
 136|     setAbortController,
 137|     onQuery,
 138|     setAppState,
 139|     onBeforeQuery,
 140|     canUseTool,
 141|     queuedCommands,
 142|     uuid,
 143|     skipSlashCommands,
 144|   } = params
 145| 
 146|   const { setCursorOffset, clearBuffer, resetHistory } = helpers
 147| 
 148|   // Queue processor path: commands are pre-validated and ready to execute.
 149|   // Skip all input validation, reference parsing, and queuing logic.
 150|   if (queuedCommands?.length) {
 151|     startQueryProfile()
 152|     await executeUserInput({
 153|       queuedCommands,
 154|       messages,
 155|       mainLoopModel,
 156|       ideSelection,
 157|       querySource: params.querySource,
 158|       commands,
 159|       queryGuard,
 160|       setToolJSX,

schema 与用户配置示例

schema.ts 描述 keybindings.json 结构:context 块 + bindings 对象。action 字符串可任意,validate 阶段检查 known app:/chat: prefix;command:* 通常 不 白名单校验(开放 ended)。

示例:

{
  "context": "Chat",
  "bindings": {
    "ctrl+shift+m": "command:memory",
    "alt+c": "command:compact"
  }
}

合并顺序:DEFAULT_BINDINGS 先 parse,用户块后 parse,同 context 同 chord 用户覆盖。

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

   1| /**
   2|  * Zod schema for keybindings.json configuration.
   3|  * Used for validation and JSON schema generation.
   4|  */
   5| 
   6| import { z } from 'zod/v4'
   7| import { lazySchema } from '../utils/lazySchema.js'
   8| 
   9| /**
  10|  * Valid context names where keybindings can be applied.
  11|  */
  12| export const KEYBINDING_CONTEXTS = [
  13|   'Global',
  14|   'Chat',
  15|   'Autocomplete',
  16|   'Confirmation',
  17|   'Help',
  18|   'Transcript',
  19|   'HistorySearch',
  20|   'Task',
  21|   'ThemePicker',
  22|   'Settings',
  23|   'Tabs',
  24|   // New contexts for keybindings migration
  25|   'Attachments',
  26|   'Footer',
  27|   'MessageSelector',
  28|   'DiffDialog',
  29|   'ModelPicker',
  30|   'Select',
  31|   'Plugin',
  32| ] as const
  33| 
  34| /**
  35|  * Human-readable descriptions for each keybinding context.
  36|  */
  37| export const KEYBINDING_CONTEXT_DESCRIPTIONS: Record<
  38|   (typeof KEYBINDING_CONTEXTS)[number],
  39|   string
  40| > = {
  41|   Global: 'Active everywhere, regardless of focus',
  42|   Chat: 'When the chat input is focused',
  43|   Autocomplete: 'When autocomplete menu is visible',
  44|   Confirmation: 'When a confirmation/permission dialog is shown',
  45|   Help: 'When the help overlay is open',
  46|   Transcript: 'When viewing the transcript',
  47|   HistorySearch: 'When searching command history (ctrl+r)',
  48|   Task: 'When a task/agent is running in the foreground',
  49|   ThemePicker: 'When the theme picker is open',
  50|   Settings: 'When the settings menu is open',

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

   1| /**
   2|  * Keybindings template generator.
   3|  * Generates a well-documented template file for ~/.claude/keybindings.json
   4|  */
   5| 
   6| import { jsonStringify } from '../utils/slowOperations.js'
   7| import { DEFAULT_BINDINGS } from './defaultBindings.js'
   8| import {
   9|   NON_REBINDABLE,
  10|   normalizeKeyForComparison,
  11| } from './reservedShortcuts.js'
  12| import type { KeybindingBlock } from './types.js'
  13| 
  14| /**
  15|  * Filter out reserved shortcuts that cannot be rebound.
  16|  * These would cause /doctor to warn, so we exclude them from the template.
  17|  */
  18| function filterReservedShortcuts(blocks: KeybindingBlock[]): KeybindingBlock[] {
  19|   const reservedKeys = new Set(
  20|     NON_REBINDABLE.map(r => normalizeKeyForComparison(r.key)),
  21|   )
  22| 
  23|   return blocks
  24|     .map(block => {
  25|       const filteredBindings: Record<string, string | null> = {}
  26|       for (const [key, action] of Object.entries(block.bindings)) {
  27|         if (!reservedKeys.has(normalizeKeyForComparison(key))) {
  28|           filteredBindings[key] = action
  29|         }
  30|       }
  31|       return { context: block.context, bindings: filteredBindings }
  32|     })
  33|     .filter(block => Object.keys(block.bindings).length > 0)
  34| }
  35| 
  36| /**
  37|  * Generate a template keybindings.json file content.
  38|  * Creates a fully valid JSON file with all default bindings that users can customize.
  39|  */
  40| export function generateKeybindingsTemplate(): string {

defaultBindings 无 command:*

开箱 defaultBindings 不包含 command:* 条目——避免把 /commit 等绑死到固定键,留给用户 customization。

产品可在文档推荐 binding,但不进 DEFAULT_BINDINGS 除非成为 universal UX(类似 ctrl+t todos)。

loadUserBindings 解析后 bindings 数组同时含 app:* 与 command:*,CommandKeybindingHandlers 只消费后者 subset。

源码引用: src/keybindings/defaultBindings.ts · 第 32–62 行(共 341 行)

  32| export const DEFAULT_BINDINGS: KeybindingBlock[] = [
  33|   {
  34|     context: 'Global',
  35|     bindings: {
  36|       // ctrl+c and ctrl+d use special time-based double-press handling.
  37|       // They ARE defined here so the resolver can find them, but they
  38|       // CANNOT be rebound by users - validation in reservedShortcuts.ts
  39|       // will show an error if users try to override these keys.
  40|       'ctrl+c': 'app:interrupt',
  41|       'ctrl+d': 'app:exit',
  42|       'ctrl+l': 'app:redraw',
  43|       'ctrl+t': 'app:toggleTodos',
  44|       'ctrl+o': 'app:toggleTranscript',
  45|       ...(feature('KAIROS') || feature('KAIROS_BRIEF')
  46|         ? { 'ctrl+shift+b': 'app:toggleBrief' as const }
  47|         : {}),
  48|       'ctrl+shift+o': 'app:toggleTeammatePreview',
  49|       'ctrl+r': 'history:search',
  50|       // File navigation. cmd+ bindings only fire on kitty-protocol terminals;
  51|       // ctrl+shift is the portable fallback.
  52|       ...(feature('QUICK_SEARCH')
  53|         ? {
  54|             'ctrl+shift+f': 'app:globalSearch' as const,
  55|             'cmd+shift+f': 'app:globalSearch' as const,
  56|             'ctrl+shift+p': 'app:quickOpen' as const,
  57|             'cmd+shift+p': 'app:quickOpen' as const,
  58|           }
  59|         : {}),
  60|       ...(feature('TERMINAL_PANEL') ? { 'meta+j': 'app:toggleTerminal' } : {}),
  61|     },
  62|   },

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

 180|         warnings: [
 181|           {
 182|             type: 'parse_error',
 183|             severity: 'error',
 184|             message: errorMessage,
 185|             suggestion,
 186|           },
 187|         ],
 188|       }
 189|     }
 190| 
 191|     const userParsed = parseBindings(userBlocks)
 192|     logForDebugging(
 193|       `[keybindings] Loaded ${userParsed.length} user bindings from ${userPath}`,
 194|     )
 195| 
 196|     // User bindings come after defaults, so they override
 197|     const mergedBindings = [...defaultBindings, ...userParsed]
 198| 
 199|     logCustomBindingsLoadedOncePerDay(userParsed.length)
 200| 
 201|     // Run validation on user config
 202|     // First check for duplicate keys in raw JSON (JSON.parse silently drops earlier values)
 203|     const duplicateKeyWarnings = checkDuplicateKeysInJson(content)
 204|     const warnings = [
 205|       ...duplicateKeyWarnings,
 206|       ...validateBindings(userBlocks, mergedBindings),
 207|     ]
 208| 
 209|     if (warnings.length > 0) {
 210|       logForDebugging(
 211|         `[keybindings] Found ${warnings.length} validation issue(s)`,
 212|       )
 213|     }
 214| 
 215|     return { bindings: mergedBindings, warnings }
 216|   } catch (error) {
 217|     // File doesn't exist - use defaults (user can run /keybindings to create)
 218|     if (isENOENT(error)) {
 219|       return { bindings: defaultBindings, warnings: [] }
 220|     }
 221| 
 222|     // Other error - log and return defaults with warning
 223|     logForDebugging(
 224|       `[keybindings] Error loading ${userPath}: ${errorMessage(error)}`,
 225|     )
 226|     return {
 227|       bindings: defaultBindings,
 228|       warnings: [
 229|         {
 230|           type: 'parse_error',
 231|           severity: 'error',
 232|           message: `Failed to parse keybindings.json: ${errorMessage(error)}`,
 233|         },
 234|       ],
 235|     }
 236|   }
 237| }
 238| 
 239| /**
 240|  * Load keybindings synchronously (for initial render).

REPL 挂载与调试

REPL.tsx 在 KeybindingSetup 内并列:

<GlobalKeybindingHandlers ... />
<CommandKeybindingHandlers onSubmit={handleSubmit} isActive={...} />

调试「command 快捷键无效」:

  1. keybindings.json 是否 parse 成功(Doctor warnings)
  2. isModalOverlayActive
  3. chord 是否未完成 pending
  4. command 名是否与 commands/registry 一致

hooks/input-keybindings 子章节提供全局键盘 UX 上下文。

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

   1| // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
   2| import { feature } from 'bun:bundle'
   3| import { spawnSync } from 'child_process'
   4| import {
   5|   snapshotOutputTokensForTurn,
   6|   getCurrentTurnTokenBudget,
   7|   getTurnOutputTokens,
   8|   getBudgetContinuationCount,
   9|   getTotalInputTokens,
  10| } from '../bootstrap/state.js'
  11| import { parseTokenBudget } from '../utils/tokenBudget.js'
  12| import { count } from '../utils/array.js'
  13| import { dirname, join } from 'path'
  14| import { tmpdir } from 'os'
  15| import figures from 'figures'
  16| // eslint-disable-next-line custom-rules/prefer-use-keybindings -- / n N Esc [ v are bare letters in transcript modal context, same class as g/G/j/k in ScrollKeybindingHandler
  17| import { useInput } from '../ink.js'
  18| import { useSearchInput } from '../hooks/useSearchInput.js'
  19| import { useTerminalSize } from '../hooks/useTerminalSize.js'
  20| import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js'
  21| import type { JumpHandle } from '../components/VirtualMessageList.js'
  22| import { renderMessagesToPlainText } from '../utils/exportRenderer.js'
  23| import { openFileInExternalEditor } from '../utils/editor.js'
  24| import { writeFile } from 'fs/promises'
  25| import {
  26|   Box,
  27|   Text,
  28|   useStdin,
  29|   useTheme,
  30|   useTerminalFocus,
  31|   useTerminalTitle,
  32|   useTabStatus,
  33| } from '../ink.js'
  34| import type { TabStatusKind } from '../ink/hooks/use-tab-status.js'
  35| import { CostThresholdDialog } from '../components/CostThresholdDialog.js'
  36| import { IdleReturnDialog } from '../components/IdleReturnDialog.js'
  37| import * as React from 'react'
  38| import {
  39|   useEffect,
  40|   useMemo,
  41|   useRef,
  42|   useState,
  43|   useCallback,
  44|   useDeferredValue,
  45|   useLayoutEffect,
  46|   type RefObject,
  47| } from 'react'
  48| import { useNotifications } from '../context/notifications.js'
  49| import { sendNotification } from '../services/notifier.js'
  50| import {

fromKeybinding 的真正影响

command:* 最容易误解的一点是:它不是把某个 React handler 直接绑到键上,而是合成一条 slash command 输入交给 onSubmit。CommandKeybindingHandlers 只从已解析 bindings 中收集 action.startsWith("command:") 的项,取 command: 后面的名字拼成 /name,再传入 NOOP_HELPERS 和 { fromKeybinding: true }。NOOP_HELPERS 保证当前 prompt 光标、buffer、历史不被这个合成输入破坏;Chat context 和 modal overlay 门控保证它只在主输入可用时触发。

REPL 侧收到 fromKeybinding 后,会把匹配命令视作 immediate 候选:如果命令是 local-jsx 且 queryGuard active,就直接打开对应 Dialog,同时保留用户原本正在输入的 prompt。代码还跳过 keybinding 触发命令的 history 记录,并在提交清空逻辑里保留输入内容。也就是说,alt+m 触发 command:memory 与用户手动输入 /memory 共享命令 registry 和 local-jsx 实现,但交互语义不同:前者是“旁路打开命令,不污染当前草稿”,后者是“用户显式提交一条 slash 命令”。排查问题时应顺着 bindings → CommandKeybindingHandlers → REPL immediate branch,而不是只在 defaultBindings 里找内置条目。

command:* 还有一个有意保留的开放性:它不要求 defaultBindings 预先列出所有 slash 命令。用户、插件或未来内置命令只要进入命令 registry,就可以被 keybindings.json 通过 command:name 触发。代价是校验阶段无法完全判断 command:unknown 是否会失败,真正的错误可能要到 handlePromptSubmit 查 registry 时才出现。文档应把它描述为“动态命令桥”,不是稳定 action 白名单;推荐配置时也要提醒命令名、别名、local-jsx 是否支持当前会话模式以及 modal 是否打开。

这个设计也解释了为什么 command-bindings 章节要引用 schema 和 defaultBindings,却不能把它们当主要实现。schema 只描述 keybindings.json 的结构,defaultBindings 只是默认表;真正决定某个 command:* 能否工作的是当前解析后的 bindings、命令 registry、REPL onSubmit 分支和 overlay 状态。若用户把 command:compact 绑定到 Chat context,按键先被 resolver 解析成 action,再由 CommandKeybindingHandlers 变成 /compact;后续 compact 是否立即执行、是否显示 local-jsx、是否清空输入,都由命令自身和 REPL 分支决定。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 37–83 行(共 83 行)

  37| 
  38| /**
  39|  * Registers keybinding handlers for all "command:*" actions found in the
  40|  * user's keybinding configuration. When triggered, each handler submits
  41|  * the corresponding slash command (e.g., "command:commit" submits "/commit").
  42|  */
  43| export function CommandKeybindingHandlers({
  44|   onSubmit,
  45|   isActive = true,
  46| }: Props): null {
  47|   const keybindingContext = useOptionalKeybindingContext()
  48|   const isModalOverlayActive = useIsModalOverlayActive()
  49| 
  50|   // Extract command actions from parsed bindings
  51|   const commandActions = useMemo(() => {
  52|     if (!keybindingContext) return new Set<string>()
  53|     const actions = new Set<string>()
  54|     for (const binding of keybindingContext.bindings) {
  55|       if (binding.action?.startsWith('command:')) {
  56|         actions.add(binding.action)
  57|       }
  58|     }
  59|     return actions
  60|   }, [keybindingContext])
  61| 
  62|   // Build handler map for all command actions
  63|   const handlers = useMemo(() => {
  64|     const map: Record<string, () => void> = {}
  65|     for (const action of commandActions) {
  66|       const commandName = action.slice('command:'.length)
  67|       map[action] = () => {
  68|         onSubmit(`/${commandName}`, NOOP_HELPERS, undefined, {
  69|           fromKeybinding: true,
  70|         })
  71|       }
  72|     }
  73|     return map
  74|   }, [commandActions, onSubmit])
  75| 
  76|   useKeybindings(handlers, {
  77|     context: 'Chat',
  78|     isActive: isActive && !isModalOverlayActive,
  79|   })
  80| 
  81|   return null
  82| }
  83| 

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

3170|       // the closure captured at render time. Also doubles as refreshTools()
3171|       // for mid-query tool list updates.
3172|       const computeTools = () => {
3173|         const state = store.getState()
3174|         const assembled = assembleToolPool(
3175|           state.toolPermissionContext,
3176|           state.mcp.tools,
3177|         )
3178|         const merged = mergeAndFilterTools(
3179|           combinedInitialTools,
3180|           assembled,
3181|           state.toolPermissionContext.mode,
3182|         )
3183|         if (!mainThreadAgentDefinition) return merged
3184|         return resolveAgentTools(mainThreadAgentDefinition, merged, false, true)
3185|           .resolvedTools
3186|       }
3187| 
3188|       return {
3189|         abortController,
3190|         options: {
3191|           commands,
3192|           tools: computeTools(),
3193|           debug,
3194|           verbose: s.verbose,
3195|           mainLoopModel,
3196|           thinkingConfig:
3197|             s.thinkingEnabled !== false ? thinkingConfig : { type: 'disabled' },
3198|           // Merge fresh from store rather than closing over useMergedClients'
3199|           // memoized output. initialMcpClients is a prop (session-constant).
3200|           mcpClients: mergeClients(initialMcpClients, s.mcp.clients),
3201|           mcpResources: s.mcp.resources,
3202|           ideInstallationStatus: ideInstallationStatus,
3203|           isNonInteractiveSession: false,
3204|           dynamicMcpConfig,
3205|           theme,

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

3313|       dynamicMcpConfig,
3314|       theme,
3315|       allowedAgentTypes,
3316|       store,
3317|       setAppState,
3318|       reverify,
3319|       addNotification,
3320|       setMessages,
3321|       onChangeDynamicMcpConfig,
3322|       resume,
3323|       requestPrompt,
3324|       disabled,
3325|       customSystemPrompt,
3326|       appendSystemPrompt,
3327|       setConversationId,
3328|     ],
3329|   )
3330| 
3331|   // Session backgrounding (Ctrl+B to background/foreground)
3332|   const handleBackgroundQuery = useCallback(() => {
3333|     // Stop the foreground query so the background one takes over
3334|     abortController?.abort('background')
3335|     // Aborting subagents may produce task-completed notifications.
3336|     // Clear task notifications so the queue processor doesn't immediately
3337|     // start a new foreground query; forward them to the background session.
3338|     const removedNotifications = removeByFilter(
3339|       cmd => cmd.mode === 'task-notification',
3340|     )
3341| 
3342|     void (async () => {
3343|       const toolUseContext = getToolUseContext(
3344|         messagesRef.current,
3345|         [],
3346|         new AbortController(),
3347|         mainLoopModel,
3348|       )
3349| 
3350|       const [defaultSystemPrompt, userContext, systemContext] =
3351|         await Promise.all([
3352|           getSystemPrompt(
3353|             toolUseContext.options.tools,

为什么选择动态扫描而非静态注册

command:* 采用“从已解析 bindings 动态扫描”而不是“代码里维护命令白名单”,核心收益是解耦:命令系统可独立演进,键位系统只认前缀协议。这样新增 slash 命令或插件命令时,不必同步改 keybindings 模块。代价是配置阶段无法完全静态校验命令名,unknown command 要到提交路径再报错。

这也是 command-bindings 与 app:* 的根本区别:app:* 是产品内建行为,追求可预测与强校验;command:* 是用户驱动入口,追求扩展性与低耦合。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 50–83 行(共 83 行)

  50|   // Extract command actions from parsed bindings
  51|   const commandActions = useMemo(() => {
  52|     if (!keybindingContext) return new Set<string>()
  53|     const actions = new Set<string>()
  54|     for (const binding of keybindingContext.bindings) {
  55|       if (binding.action?.startsWith('command:')) {
  56|         actions.add(binding.action)
  57|       }
  58|     }
  59|     return actions
  60|   }, [keybindingContext])
  61| 
  62|   // Build handler map for all command actions
  63|   const handlers = useMemo(() => {
  64|     const map: Record<string, () => void> = {}
  65|     for (const action of commandActions) {
  66|       const commandName = action.slice('command:'.length)
  67|       map[action] = () => {
  68|         onSubmit(`/${commandName}`, NOOP_HELPERS, undefined, {
  69|           fromKeybinding: true,
  70|         })
  71|       }
  72|     }
  73|     return map
  74|   }, [commandActions, onSubmit])
  75| 
  76|   useKeybindings(handlers, {
  77|     context: 'Chat',
  78|     isActive: isActive && !isModalOverlayActive,
  79|   })
  80| 
  81|   return null
  82| }
  83| 

源码引用: src/utils/handlePromptSubmit.ts · 第 100–160 行(共 611 行)

 100|     React.SetStateAction<Record<number, PastedContent>>
 101|   >
 102|   abortController?: AbortController | null
 103|   addNotification?: (notification: {
 104|     key: string
 105|     text: string
 106|     priority: 'low' | 'medium' | 'high' | 'immediate'
 107|   }) => void
 108|   setMessages?: (updater: (prev: Message[]) => Message[]) => void
 109|   streamMode?: SpinnerMode
 110|   hasInterruptibleToolInProgress?: boolean
 111|   uuid?: UUID
 112|   /**
 113|    * When true, input starting with `/` is treated as plain text.
 114|    * Used for remotely-received messages (bridge/CCR) that should not
 115|    * trigger local slash commands or skills.
 116|    */
 117|   skipSlashCommands?: boolean
 118| }
 119| 
 120| export async function handlePromptSubmit(
 121|   params: HandlePromptSubmitParams,
 122| ): Promise<void> {
 123|   const {
 124|     helpers,
 125|     queryGuard,
 126|     isExternalLoading = false,
 127|     commands,
 128|     onInputChange,
 129|     setPastedContents,
 130|     setToolJSX,
 131|     getToolUseContext,
 132|     messages,
 133|     mainLoopModel,
 134|     ideSelection,
 135|     setUserInputOnProcessing,
 136|     setAbortController,
 137|     onQuery,
 138|     setAppState,
 139|     onBeforeQuery,
 140|     canUseTool,
 141|     queuedCommands,
 142|     uuid,
 143|     skipSlashCommands,
 144|   } = params
 145| 
 146|   const { setCursorOffset, clearBuffer, resetHistory } = helpers
 147| 
 148|   // Queue processor path: commands are pre-validated and ready to execute.
 149|   // Skip all input validation, reference parsing, and queuing logic.
 150|   if (queuedCommands?.length) {
 151|     startQueryProfile()
 152|     await executeUserInput({
 153|       queuedCommands,
 154|       messages,
 155|       mainLoopModel,
 156|       ideSelection,
 157|       querySource: params.querySource,
 158|       commands,
 159|       queryGuard,
 160|       setToolJSX,

源码引用: src/keybindings/defaultBindings.ts · 第 32–62 行(共 341 行)

  32| export const DEFAULT_BINDINGS: KeybindingBlock[] = [
  33|   {
  34|     context: 'Global',
  35|     bindings: {
  36|       // ctrl+c and ctrl+d use special time-based double-press handling.
  37|       // They ARE defined here so the resolver can find them, but they
  38|       // CANNOT be rebound by users - validation in reservedShortcuts.ts
  39|       // will show an error if users try to override these keys.
  40|       'ctrl+c': 'app:interrupt',
  41|       'ctrl+d': 'app:exit',
  42|       'ctrl+l': 'app:redraw',
  43|       'ctrl+t': 'app:toggleTodos',
  44|       'ctrl+o': 'app:toggleTranscript',
  45|       ...(feature('KAIROS') || feature('KAIROS_BRIEF')
  46|         ? { 'ctrl+shift+b': 'app:toggleBrief' as const }
  47|         : {}),
  48|       'ctrl+shift+o': 'app:toggleTeammatePreview',
  49|       'ctrl+r': 'history:search',
  50|       // File navigation. cmd+ bindings only fire on kitty-protocol terminals;
  51|       // ctrl+shift is the portable fallback.
  52|       ...(feature('QUICK_SEARCH')
  53|         ? {
  54|             'ctrl+shift+f': 'app:globalSearch' as const,
  55|             'cmd+shift+f': 'app:globalSearch' as const,
  56|             'ctrl+shift+p': 'app:quickOpen' as const,
  57|             'cmd+shift+p': 'app:quickOpen' as const,
  58|           }
  59|         : {}),
  60|       ...(feature('TERMINAL_PANEL') ? { 'meta+j': 'app:toggleTerminal' } : {}),
  61|     },
  62|   },

最小可用示例与回滚

实践上推荐先给 Chat context 增加一条低风险绑定(如 alt+m → command:memory),验证 modal 门控、草稿保留和命令可达后,再批量增加其它 command:*。若出现误触发,优先在 keybindings.json 删除对应 action 即可快速回滚,不需要改命令实现。

源码引用: src/hooks/useCommandKeybindings.tsx · 第 93–83 行(共 83 行)


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

   1| /**
   2|  * Keybindings template generator.
   3|  * Generates a well-documented template file for ~/.claude/keybindings.json
   4|  */
   5| 
   6| import { jsonStringify } from '../utils/slowOperations.js'
   7| import { DEFAULT_BINDINGS } from './defaultBindings.js'
   8| import {
   9|   NON_REBINDABLE,
  10|   normalizeKeyForComparison,
  11| } from './reservedShortcuts.js'
  12| import type { KeybindingBlock } from './types.js'
  13| 
  14| /**
  15|  * Filter out reserved shortcuts that cannot be rebound.
  16|  * These would cause /doctor to warn, so we exclude them from the template.
  17|  */
  18| function filterReservedShortcuts(blocks: KeybindingBlock[]): KeybindingBlock[] {
  19|   const reservedKeys = new Set(
  20|     NON_REBINDABLE.map(r => normalizeKeyForComparison(r.key)),
  21|   )
  22| 
  23|   return blocks
  24|     .map(block => {
  25|       const filteredBindings: Record<string, string | null> = {}
  26|       for (const [key, action] of Object.entries(block.bindings)) {
  27|         if (!reservedKeys.has(normalizeKeyForComparison(key))) {
  28|           filteredBindings[key] = action
  29|         }
  30|       }
  31|       return { context: block.context, bindings: filteredBindings }
  32|     })
  33|     .filter(block => Object.keys(block.bindings).length > 0)
  34| }
  35| 
  36| /**
  37|  * Generate a template keybindings.json file content.
  38|  * Creates a fully valid JSON file with all default bindings that users can customize.
  39|  */
  40| export function generateKeybindingsTemplate(): string {

本章小结与延伸

command-bindings 连接用户 JSON 配置与 slash 命令执行器。下一章 vim-bindings 说明为何不把所有编辑键迁入 keybindings。 继续学习:

  • vim-bindings
  • default-bindings
Prev
default-bindings · 默认键位表与平台差异
Next
vim-bindings · Vim 模式与 keybindings 边界