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

本章总览

先记住定位:default-bindings 是产品默认键盘体验的单一真相源,定义“开箱可用动作 + context 边界 + 平台兼容策略”。在这张表之上,用户配置才做覆盖,校验层再通过 reserved 列表守住不可重绑按键。

学完本章你应该能

  • 列举 Global 与 Chat context 的主要 action
  • 解释 IMAGE_PASTE_KEY 与 MODE_CYCLE_KEY 平台分支
  • 理解 ctrl+c/d 在 default 表存在但 reserved 不可改
  • 知道 feature gate 如何展开 QUICK_SEARCH、VOICE_MODE 等块
  • 区分 Settings 与 Select context 复用 select:* 动作
  • 能用 useShortcutDisplay 反查 action 展示字符串

核心概念(先读懂这些)

Action 字符串是稳定 API

app:toggleTodos、chat:cycleMode 等字符串在 useGlobalKeybindings 与 defaultBindings 间契约化。新增 action 需同时:defaultBindings 条目、GlobalKeybindingHandlers 注册、schema 文档(若有)、Doctor 帮助表。

Context 隔离冲突键

enter 在 Chat 是 submit,在 Settings 是 close,在 Confirmation 是 yes——靠 context + activeContexts 隔离,而非不同物理键。Scroll context 处理 wheel/page 键;Transcript 用 q 退出(pager 惯例)。

Feature 条件 spread

MESSAGE_ACTIONS、QUICK_SEARCH、KAIROS、TERMINAL_PANEL、VOICE_MODE 等用 feature() 或 satisfies 展开 binding 对象。未编译进的 feature 在 bundle 中 DCE,对应 action 无 default 键但仍可被用户 JSON 绑定(若 customization 开启)。

建议学习步骤

  1. 阅读文件头 IMAGE_PASTE_KEY / MODE_CYCLE_KEY 逻辑
  2. 浏览 Global + Chat 两块 bindings
  3. 继续读 Transcript、HistorySearch、Scroll
  4. 查看 MESSAGE_ACTIONS 条件块
  5. 打开 reservedShortcuts 对照 ctrl+c
  6. 在 useGlobalKeybindings 映射 action 到 handler

常见误区

注意

Windows 无 VT 时 shift+tab 不可靠——用 meta+m

注意

voice:pushToTalk 绑定 space 时 unbound 陷阱见注释

注意

Settings 的 enter 保存关闭,space 才是 toggle

注意

Plugin context 仅 space/i,导航走 Select context

平台相关常量

IMAGE_PASTE_KEY:Windows 为 alt+v(ctrl+v 是系统粘贴),其它平台 ctrl+v。

SUPPORTS_TERMINAL_VT_MODE:Windows 上检测 Bun>=1.2.23 或 Node>=22.17/24.2+,否则 MODE_CYCLE_KEY=meta+m 代替 shift+tab。

注释引用 Windows Terminal VT issue 与 Node/Bun PR——mode cycle 是终端兼容性问题,不是 keybindings 解析 bug。

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

  12| // Platform-specific image paste shortcut:
  13| // - Windows: alt+v (ctrl+v is system paste)
  14| // - Other platforms: ctrl+v
  15| const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'
  16| 
  17| // Modifier-only chords (like shift+tab) may fail on Windows Terminal without VT mode
  18| // See: https://github.com/microsoft/terminal/issues/879#issuecomment-618801651
  19| // Node enabled VT mode in 24.2.0 / 22.17.0: https://github.com/nodejs/node/pull/58358
  20| // Bun enabled VT mode in 1.2.23: https://github.com/oven-sh/bun/pull/21161
  21| const SUPPORTS_TERMINAL_VT_MODE =
  22|   getPlatform() !== 'windows' ||
  23|   (isRunningWithBun()
  24|     ? satisfies(process.versions.bun, '>=1.2.23')
  25|     : satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))
  26| 
  27| // Platform-specific mode cycle shortcut:
  28| // - Windows without VT mode: meta+m (shift+tab doesn't work reliably)
  29| // - Other platforms: shift+tab
  30| const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'

Global context

键Action行为
ctrl+capp:interrupt中断(reserved)
ctrl+dapp:exit退出(reserved)
ctrl+lapp:redraw重绘
ctrl+tapp:toggleTodos任务面板
ctrl+oapp:toggleTranscriptTranscript 模态
ctrl+shift+oapp:toggleTeammatePreview队友预览
ctrl+rhistory:search历史搜索
ctrl+shift+f/papp:globalSearch/quickOpenQUICK_SEARCH feature
meta+japp:toggleTerminalTERMINAL_PANEL

KAIROS feature 追加 ctrl+shift+b → app:toggleBrief。

源码引用: 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

Chat 块覆盖 prompt 行编辑与提交:

  • escape → chat:cancel
  • ctrl+x ctrl+k → chat:killAgents(chord 避免 shadow readline)
  • MODE_CYCLE_KEY → chat:cycleMode(权限模式循环)
  • meta+p/o/t → modelPicker、fastMode、thinkingToggle
  • enter → chat:submit;up/down → history
  • ctrl+_ 与 ctrl+shift+- 双绑 chat:undo
  • ctrl+x ctrl+e、ctrl+g → externalEditor
  • ctrl+s → chat:stash
  • IMAGE_PASTE_KEY → chat:imagePaste
  • VOICE_MODE:space → voice:pushToTalk

MESSAGE_ACTIONS:shift+up → chat:messageActions。

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

  63|   {
  64|     context: 'Chat',
  65|     bindings: {
  66|       escape: 'chat:cancel',
  67|       // ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
  68|       'ctrl+x ctrl+k': 'chat:killAgents',
  69|       [MODE_CYCLE_KEY]: 'chat:cycleMode',
  70|       'meta+p': 'chat:modelPicker',
  71|       'meta+o': 'chat:fastMode',
  72|       'meta+t': 'chat:thinkingToggle',
  73|       enter: 'chat:submit',
  74|       up: 'history:previous',
  75|       down: 'history:next',
  76|       // Editing shortcuts (defined here, migration in progress)
  77|       // Undo has two bindings to support different terminal behaviors:
  78|       // - ctrl+_ for legacy terminals (send \x1f control char)
  79|       // - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
  80|       'ctrl+_': 'chat:undo',
  81|       'ctrl+shift+-': 'chat:undo',
  82|       // ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
  83|       'ctrl+x ctrl+e': 'chat:externalEditor',
  84|       'ctrl+g': 'chat:externalEditor',
  85|       'ctrl+s': 'chat:stash',
  86|       // Image paste shortcut (platform-specific key defined above)
  87|       [IMAGE_PASTE_KEY]: 'chat:imagePaste',
  88|       ...(feature('MESSAGE_ACTIONS')
  89|         ? { 'shift+up': 'chat:messageActions' as const }
  90|         : {}),
  91|       // Voice activation (hold-to-talk). Registered so getShortcutDisplay
  92|       // finds it without hitting the fallback analytics log. To rebind,
  93|       // add a voice:pushToTalk entry (last wins); to disable, use /voice
  94|       // — null-unbinding space hits a pre-existing useKeybinding.ts trap
  95|       // where 'unbound' swallows the event (space dead for typing).
  96|       ...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
  97|     },
  98|   },

Settings、Confirmation、Autocomplete

Autocomplete:tab accept、dismiss escape、上下项。

Settings:escape → confirm:no;上下/k/j 复用 select:*;space toggle;enter settings:close;/ 搜索;r retry usage。

Confirmation:y/n、enter/escape、shift+tab cycleMode、ctrl+e 解释、ctrl+d permission debug。

这些 context 在对应 Dialog mount 时 registerActiveContext,关闭时 unregister。

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

  99|   {
 100|     context: 'Autocomplete',
 101|     bindings: {
 102|       tab: 'autocomplete:accept',
 103|       escape: 'autocomplete:dismiss',
 104|       up: 'autocomplete:previous',
 105|       down: 'autocomplete:next',
 106|     },
 107|   },
 108|   {
 109|     context: 'Settings',
 110|     bindings: {
 111|       // Settings menu uses escape only (not 'n') to dismiss
 112|       escape: 'confirm:no',
 113|       // Config panel list navigation (reuses Select actions)
 114|       up: 'select:previous',
 115|       down: 'select:next',
 116|       k: 'select:previous',
 117|       j: 'select:next',
 118|       'ctrl+p': 'select:previous',
 119|       'ctrl+n': 'select:next',
 120|       // Toggle/activate the selected setting (space only — enter saves & closes)
 121|       space: 'select:accept',
 122|       // Save and close the config panel
 123|       enter: 'settings:close',
 124|       // Enter search mode
 125|       '/': 'settings:search',
 126|       // Retry loading usage data (only active on error)
 127|       r: 'settings:retry',
 128|     },
 129|   },
 130|   {
 131|     context: 'Confirmation',
 132|     bindings: {
 133|       y: 'confirm:yes',
 134|       n: 'confirm:no',
 135|       enter: 'confirm:yes',
 136|       escape: 'confirm:no',
 137|       // Navigation for dialogs with lists
 138|       up: 'confirm:previous',
 139|       down: 'confirm:next',
 140|       tab: 'confirm:nextField',
 141|       space: 'confirm:toggle',
 142|       // Cycle modes (used in file permission dialogs and teams dialog)
 143|       'shift+tab': 'confirm:cycleMode',
 144|       // Toggle permission explanation in permission dialogs
 145|       'ctrl+e': 'confirm:toggleExplanation',
 146|       // Toggle permission debug info
 147|       'ctrl+d': 'permission:toggleDebug',
 148|     },
 149|   },

Transcript、Scroll、Footer 等

Transcript:ctrl+e toggleShowAll、ctrl+c/escape/q exit。

HistorySearch:ctrl+r next、enter execute、escape accept。

Scroll:page/wheel、ctrl+home/end、selection:copy(kitty cmd+c)。

Footer:vim 式 j/k 与 enter openSelected。

MessageSelector / MessageActions / DiffDialog / ModelPicker / Plugin:各 dialog 专用 action 前缀,减少 Global 污染。

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

 160|   {
 161|     context: 'Transcript',
 162|     bindings: {
 163|       'ctrl+e': 'transcript:toggleShowAll',
 164|       'ctrl+c': 'transcript:exit',
 165|       escape: 'transcript:exit',
 166|       // q — pager convention (less, tmux copy-mode). Transcript is a modal
 167|       // reading view with no prompt, so q-as-literal-char has no owner.
 168|       q: 'transcript:exit',
 169|     },
 170|   },
 171|   {
 172|     context: 'HistorySearch',
 173|     bindings: {
 174|       'ctrl+r': 'historySearch:next',
 175|       escape: 'historySearch:accept',
 176|       tab: 'historySearch:accept',
 177|       'ctrl+c': 'historySearch:cancel',
 178|       enter: 'historySearch:execute',
 179|     },
 180|   },
 181|   {
 182|     context: 'Task',
 183|     bindings: {
 184|       // Background running foreground tasks (bash commands, agents)
 185|       // In tmux, users must press ctrl+b twice (tmux prefix escape)
 186|       'ctrl+b': 'task:background',
 187|     },
 188|   },
 189|   {
 190|     context: 'ThemePicker',
 191|     bindings: {
 192|       'ctrl+t': 'theme:toggleSyntaxHighlighting',
 193|     },
 194|   },
 195|   {
 196|     context: 'Scroll',
 197|     bindings: {
 198|       pageup: 'scroll:pageUp',
 199|       pagedown: 'scroll:pageDown',
 200|       wheelup: 'scroll:lineUp',
 201|       wheeldown: 'scroll:lineDown',
 202|       'ctrl+home': 'scroll:top',
 203|       'ctrl+end': 'scroll:bottom',
 204|       // Selection copy. ctrl+shift+c is standard terminal copy.
 205|       // cmd+c only fires on terminals using the kitty keyboard
 206|       // protocol (kitty/WezTerm/ghostty/iTerm2) where the super
 207|       // modifier actually reaches the pty — inert elsewhere.
 208|       // Esc-to-clear and contextual ctrl+c are handled via raw
 209|       // useInput so they can conditionally propagate.
 210|       'ctrl+shift+c': 'selection:copy',
 211|       'cmd+c': 'selection:copy',
 212|     },
 213|   },
 214|   {
 215|     context: 'Help',
 216|     bindings: {
 217|       escape: 'help:dismiss',
 218|     },
 219|   },
 220|   // Attachment navigation (select dialog image attachments)

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

 246|   // Message selector (rewind dialog) navigation
 247|   {
 248|     context: 'MessageSelector',
 249|     bindings: {
 250|       up: 'messageSelector:up',
 251|       down: 'messageSelector:down',
 252|       k: 'messageSelector:up',
 253|       j: 'messageSelector:down',
 254|       'ctrl+p': 'messageSelector:up',
 255|       'ctrl+n': 'messageSelector:down',
 256|       'ctrl+up': 'messageSelector:top',
 257|       'shift+up': 'messageSelector:top',
 258|       'meta+up': 'messageSelector:top',
 259|       'shift+k': 'messageSelector:top',
 260|       'ctrl+down': 'messageSelector:bottom',
 261|       'shift+down': 'messageSelector:bottom',
 262|       'meta+down': 'messageSelector:bottom',
 263|       'shift+j': 'messageSelector:bottom',
 264|       enter: 'messageSelector:select',
 265|     },
 266|   },
 267|   // PromptInput unmounts while cursor active — no key conflict.
 268|   ...(feature('MESSAGE_ACTIONS')
 269|     ? [
 270|         {
 271|           context: 'MessageActions' as const,
 272|           bindings: {
 273|             up: 'messageActions:prev' as const,
 274|             down: 'messageActions:next' as const,
 275|             k: 'messageActions:prev' as const,
 276|             j: 'messageActions:next' as const,
 277|             // meta = cmd on macOS; super for kitty keyboard-protocol — bind both.
 278|             'meta+up': 'messageActions:top' as const,
 279|             'meta+down': 'messageActions:bottom' as const,
 280|             'super+up': 'messageActions:top' as const,
 281|             'super+down': 'messageActions:bottom' as const,
 282|             // Mouse selection extends on shift+arrow (ScrollKeybindingHandler:573) when present —
 283|             // correct layered UX: esc clears selection, then shift+↑ jumps.
 284|             'shift+up': 'messageActions:prevUser' as const,
 285|             'shift+down': 'messageActions:nextUser' as const,
 286|             escape: 'messageActions:escape' as const,
 287|             'ctrl+c': 'messageActions:ctrlc' as const,
 288|             // Mirror MESSAGE_ACTIONS. Not imported — would pull React/ink into this config module.
 289|             enter: 'messageActions:enter' as const,
 290|             c: 'messageActions:c' as const,
 291|             p: 'messageActions:p' as const,
 292|           },
 293|         },
 294|       ]
 295|     : []),
 296|   // Diff dialog navigation
 297|   {
 298|     context: 'DiffDialog',
 299|     bindings: {
 300|       escape: 'diff:dismiss',
 301|       left: 'diff:previousSource',
 302|       right: 'diff:nextSource',
 303|       up: 'diff:previousFile',
 304|       down: 'diff:nextFile',
 305|       enter: 'diff:viewDetails',
 306|       // Note: diff:back is handled by left arrow in detail mode
 307|     },
 308|   },
 309|   // Model picker effort cycling (ant-only)
 310|   {
 311|     context: 'ModelPicker',
 312|     bindings: {
 313|       left: 'modelPicker:decreaseEffort',
 314|       right: 'modelPicker:increaseEffort',
 315|     },
 316|   },
 317|   // Select component navigation (used by /model, /resume, permission prompts, etc.)
 318|   {
 319|     context: 'Select',
 320|     bindings: {
 321|       up: 'select:previous',
 322|       down: 'select:next',
 323|       j: 'select:next',
 324|       k: 'select:previous',
 325|       'ctrl+n': 'select:next',
 326|       'ctrl+p': 'select:previous',
 327|       enter: 'select:accept',
 328|       escape: 'select:cancel',
 329|     },
 330|   },
 331|   // Plugin dialog actions (manage, browse, discover plugins)
 332|   // Navigation (select:*) uses the Select context above
 333|   {
 334|     context: 'Plugin',
 335|     bindings: {
 336|       space: 'plugin:toggle',
 337|       i: 'plugin:install',
 338|     },
 339|   },
 340| ]

reservedShortcuts 与校验

用户 keybindings.json 尝试绑定 reserved 键时,validateBindings 产出 error,KeybindingSetup 通过 notifications 提示 /doctor。

ctrl+c 在 Transcript context 映射 transcript:exit,在 Chat 仍可能被 Global interrupt 逻辑特殊处理——双重按压时间窗口在 useGlobalKeybindings 实现,不在 defaultBindings。

shortcutFormat.ts / useShortcutDisplay.ts 把 ParsedBinding 转为平台友好显示(Mac meta 符号等)。

源码引用: src/keybindings/reservedShortcuts.ts · 第 1–50 行(共 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
  41|  * - We use ctrl+s for the stash feature
  42|  */
  43| export const TERMINAL_RESERVED: ReservedShortcut[] = [
  44|   {
  45|     key: 'ctrl+z',
  46|     reason: 'Unix process suspend (SIGTSTP)',
  47|     severity: 'warning',
  48|   },
  49|   {
  50|     key: 'ctrl+\\',

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

   1| import { useEffect, useRef } from 'react'
   2| import {
   3|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
   4|   logEvent,
   5| } from '../services/analytics/index.js'
   6| import { useOptionalKeybindingContext } from './KeybindingContext.js'
   7| import type { KeybindingContextName } from './types.js'
   8| 
   9| // TODO(keybindings-migration): Remove fallback parameter after migration is complete
  10| // and we've confirmed no 'keybinding_fallback_used' events are being logged.
  11| // The fallback exists as a safety net during migration - if bindings fail to load
  12| // or an action isn't found, we fall back to hardcoded values. Once stable, callers
  13| // should be able to trust that getBindingDisplayText always returns a value for
  14| // known actions, and we can remove this defensive pattern.
  15| 
  16| /**
  17|  * Hook to get the display text for a configured shortcut.
  18|  * Returns the configured binding or a fallback if unavailable.
  19|  *
  20|  * @param action - The action name (e.g., 'app:toggleTranscript')
  21|  * @param context - The keybinding context (e.g., 'Global')
  22|  * @param fallback - Fallback text if keybinding context unavailable
  23|  * @returns The configured shortcut display text
  24|  *
  25|  * @example
  26|  * const expandShortcut = useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o')
  27|  * // Returns the user's configured binding, or 'ctrl+o' as default
  28|  */
  29| export function useShortcutDisplay(
  30|   action: string,
  31|   context: KeybindingContextName,
  32|   fallback: string,
  33| ): string {
  34|   const keybindingContext = useOptionalKeybindingContext()
  35|   const resolved = keybindingContext?.getDisplayText(action, context)
  36|   const isFallback = resolved === undefined
  37|   const reason = keybindingContext ? 'action_not_found' : 'no_context'
  38| 
  39|   // Log fallback usage once per mount (not on every render) to avoid
  40|   // flooding analytics with events from frequent re-renders.

与 useGlobalKeybindings 的映射

hooks/useGlobalKeybindings.tsx 为每个 app:* / chat:* / transcript:* 注册 useKeybinding handler,内部调 setAppState 或 REPL callbacks。

改 defaultBindings 而不注册 handler → 按键 resolve 成功但无效果;改 handler 而不更新 default → getDisplayText 回退 analytics log。

新增 Global 快捷键的标准 PR 顺序:defaultBindings → useGlobalKeybindings → 帮助文档 → schema template。

源码引用: src/hooks/useGlobalKeybindings.tsx · 第 1–80 行(共 265 行)

   1| /**
   2|  * Component that registers global keybinding handlers.
   3|  *
   4|  * Must be rendered inside KeybindingSetup to have access to the keybinding context.
   5|  * This component renders nothing - it just registers the keybinding handlers.
   6|  */
   7| import { feature } from 'bun:bundle'
   8| import { useCallback } from 'react'
   9| import instances from '../ink/instances.js'
  10| import { useKeybinding } from '../keybindings/useKeybinding.js'
  11| import type { Screen } from '../screens/REPL.js'
  12| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
  13| import {
  14|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  15|   logEvent,
  16| } from '../services/analytics/index.js'
  17| import { useAppState, useSetAppState } from '../state/AppState.js'
  18| import { count } from '../utils/array.js'
  19| import { getTerminalPanel } from '../utils/terminalPanel.js'
  20| 
  21| type Props = {
  22|   screen: Screen
  23|   setScreen: React.Dispatch<React.SetStateAction<Screen>>
  24|   showAllInTranscript: boolean
  25|   setShowAllInTranscript: React.Dispatch<React.SetStateAction<boolean>>
  26|   messageCount: number
  27|   onEnterTranscript?: () => void
  28|   onExitTranscript?: () => void
  29|   virtualScrollActive?: boolean
  30|   searchBarOpen?: boolean
  31| }
  32| 
  33| /**
  34|  * Registers global keybinding handlers for:
  35|  * - ctrl+t: Toggle todo list
  36|  * - ctrl+o: Toggle transcript mode
  37|  * - ctrl+e: Toggle showing all messages in transcript
  38|  * - ctrl+c/escape: Exit transcript mode
  39|  */
  40| export function GlobalKeybindingHandlers({
  41|   screen,
  42|   setScreen,
  43|   showAllInTranscript,
  44|   setShowAllInTranscript,
  45|   messageCount,
  46|   onEnterTranscript,
  47|   onExitTranscript,
  48|   virtualScrollActive,
  49|   searchBarOpen = false,
  50| }: Props): null {
  51|   const expandedView = useAppState(s => s.expandedView)
  52|   const setAppState = useSetAppState()
  53| 
  54|   // Toggle todo list (ctrl+t) - cycles through views
  55|   const handleToggleTodos = useCallback(() => {
  56|     logEvent('tengu_toggle_todos', {
  57|       is_expanded: expandedView === 'tasks',
  58|     })
  59|     setAppState(prev => {
  60|       const { getAllInProcessTeammateTasks } =
  61|         // eslint-disable-next-line @typescript-eslint/no-require-imports
  62|         require('../tasks/InProcessTeammateTask/InProcessTeammateTask.js') as typeof import('../tasks/InProcessTeammateTask/InProcessTeammateTask.js')
  63|       const hasTeammates =
  64|         count(
  65|           getAllInProcessTeammateTasks(prev.tasks),
  66|           t => t.status === 'running',
  67|         ) > 0
  68| 
  69|       if (hasTeammates) {
  70|         // Both exist: none → tasks → teammates → none
  71|         switch (prev.expandedView) {
  72|           case 'none':
  73|             return { ...prev, expandedView: 'tasks' as const }
  74|           case 'tasks':
  75|             return { ...prev, expandedView: 'teammates' as const }
  76|           case 'teammates':
  77|             return { ...prev, expandedView: 'none' as const }
  78|         }
  79|       }
  80|       // Only tasks: none ↔ tasks

默认表是产品行为,不只是示例

DEFAULT_BINDINGS 先于用户 keybindings.json 解析,用户块再覆盖,所以默认表既是开箱 UX,也是一份 action 名称清单。这里的键位不是随意示例:ctrl+c、ctrl+d 虽写在 Global 中,却被 reservedShortcuts 禁止重绑;Chat 的 ctrl+x chord 专门避开 readline 常用 ctrl+a/b/e/f;IMAGE_PASTE_KEY 与 MODE_CYCLE_KEY 把终端平台差异封装在文件头,避免每个 handler 自己判断 Windows、VT 模式或 Kitty 协议。

读每个 context 时要同时看“键位冲突”和“激活范围”。enter 在 Chat 是 submit,在 Settings 是 close,在 Confirmation 是 yes,这不是 resolver 的特殊规则,而是 context 隔离带来的结果。Transcript、HistorySearch、Scroll、Footer、Plugin 等 dialog 专用块让 Global 保持精简;feature gate 展开的 QUICK_SEARCH、VOICE_MODE、TERMINAL_PANEL 等条目说明 default 表还承担实验开关的落点。新增默认键位时不能只改这里,还要确认 action handler、显示文案、reserved 校验和用户模板是否同步,否则用户会看到快捷键提示但按下无效,或能绑定到一个没有消费方的 action。

默认表也记录了迁移中的历史包袱。Chat 里仍有 ctrl+_、ctrl+shift+-、ctrl+x ctrl+e、ctrl+g 等编辑相关动作,注释说明它们要兼容不同终端和 readline 习惯;voice:pushToTalk 的 space 绑定旁边写明 null-unbind 会触发 unbound 吞键陷阱。看到这些注释时,不应简单把键位整理成“更规整”的表,而要理解它们对应真实终端差异和旧用户习惯。默认键位的改动属于产品行为变更,需要比新增用户自定义示例更谨慎。

验证默认键位时,最好同时检查显示层。getDisplayText 依赖解析后的最后匹配项,如果默认键被 feature gate 移除或被用户覆盖,帮助文案和 footer 提示会随之变化。

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

   7| /**
   8|  * Default keybindings that match current Claude Code behavior.
   9|  * These are loaded first, then user keybindings.json overrides them.
  10|  */
  11| 
  12| // Platform-specific image paste shortcut:
  13| // - Windows: alt+v (ctrl+v is system paste)
  14| // - Other platforms: ctrl+v
  15| const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'
  16| 
  17| // Modifier-only chords (like shift+tab) may fail on Windows Terminal without VT mode
  18| // See: https://github.com/microsoft/terminal/issues/879#issuecomment-618801651
  19| // Node enabled VT mode in 24.2.0 / 22.17.0: https://github.com/nodejs/node/pull/58358
  20| // Bun enabled VT mode in 1.2.23: https://github.com/oven-sh/bun/pull/21161
  21| const SUPPORTS_TERMINAL_VT_MODE =
  22|   getPlatform() !== 'windows' ||
  23|   (isRunningWithBun()
  24|     ? satisfies(process.versions.bun, '>=1.2.23')
  25|     : satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))
  26| 
  27| // Platform-specific mode cycle shortcut:
  28| // - Windows without VT mode: meta+m (shift+tab doesn't work reliably)
  29| // - Other platforms: shift+tab
  30| const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'

源码引用: src/keybindings/defaultBindings.ts · 第 32–98 行(共 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|   },
  63|   {
  64|     context: 'Chat',
  65|     bindings: {
  66|       escape: 'chat:cancel',
  67|       // ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
  68|       'ctrl+x ctrl+k': 'chat:killAgents',
  69|       [MODE_CYCLE_KEY]: 'chat:cycleMode',
  70|       'meta+p': 'chat:modelPicker',
  71|       'meta+o': 'chat:fastMode',
  72|       'meta+t': 'chat:thinkingToggle',
  73|       enter: 'chat:submit',
  74|       up: 'history:previous',
  75|       down: 'history:next',
  76|       // Editing shortcuts (defined here, migration in progress)
  77|       // Undo has two bindings to support different terminal behaviors:
  78|       // - ctrl+_ for legacy terminals (send \x1f control char)
  79|       // - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
  80|       'ctrl+_': 'chat:undo',
  81|       'ctrl+shift+-': 'chat:undo',
  82|       // ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
  83|       'ctrl+x ctrl+e': 'chat:externalEditor',
  84|       'ctrl+g': 'chat:externalEditor',
  85|       'ctrl+s': 'chat:stash',
  86|       // Image paste shortcut (platform-specific key defined above)
  87|       [IMAGE_PASTE_KEY]: 'chat:imagePaste',
  88|       ...(feature('MESSAGE_ACTIONS')
  89|         ? { 'shift+up': 'chat:messageActions' as const }
  90|         : {}),
  91|       // Voice activation (hold-to-talk). Registered so getShortcutDisplay
  92|       // finds it without hitting the fallback analytics log. To rebind,
  93|       // add a voice:pushToTalk entry (last wins); to disable, use /voice
  94|       // — null-unbinding space hits a pre-existing useKeybinding.ts trap
  95|       // where 'unbound' swallows the event (space dead for typing).
  96|       ...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
  97|     },
  98|   },

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

  40| export function generateKeybindingsTemplate(): string {
  41|   // Filter out reserved shortcuts that cannot be rebound
  42|   const bindings = filterReservedShortcuts(DEFAULT_BINDINGS)
  43| 
  44|   // Format as object wrapper with bindings array

新增默认键位的变更清单

给 DEFAULT_BINDINGS 新增一条记录时,建议把它当产品行为变更处理,而不是配置补丁。最少需要检查四件事:是否已有同 context 冲突键;对应 action 是否有 handler;是否触碰 reservedShortcuts;快捷键展示与帮助文案是否同步。若键位挂在 feature gate 下,还要验证 feature 关闭时不会遗留无效展示文本。

这份清单的价值在于减少“表里有、运行时无”的假阳性。很多线上问题不是 resolver 失效,而是 action 没有注册消费方,或者门控条件导致默认键从未生效。

源码引用: src/keybindings/defaultBindings.ts · 第 32–149 行(共 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|   },
  63|   {
  64|     context: 'Chat',
  65|     bindings: {
  66|       escape: 'chat:cancel',
  67|       // ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
  68|       'ctrl+x ctrl+k': 'chat:killAgents',
  69|       [MODE_CYCLE_KEY]: 'chat:cycleMode',
  70|       'meta+p': 'chat:modelPicker',
  71|       'meta+o': 'chat:fastMode',
  72|       'meta+t': 'chat:thinkingToggle',
  73|       enter: 'chat:submit',
  74|       up: 'history:previous',
  75|       down: 'history:next',
  76|       // Editing shortcuts (defined here, migration in progress)
  77|       // Undo has two bindings to support different terminal behaviors:
  78|       // - ctrl+_ for legacy terminals (send \x1f control char)
  79|       // - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
  80|       'ctrl+_': 'chat:undo',
  81|       'ctrl+shift+-': 'chat:undo',
  82|       // ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
  83|       'ctrl+x ctrl+e': 'chat:externalEditor',
  84|       'ctrl+g': 'chat:externalEditor',
  85|       'ctrl+s': 'chat:stash',
  86|       // Image paste shortcut (platform-specific key defined above)
  87|       [IMAGE_PASTE_KEY]: 'chat:imagePaste',
  88|       ...(feature('MESSAGE_ACTIONS')
  89|         ? { 'shift+up': 'chat:messageActions' as const }
  90|         : {}),
  91|       // Voice activation (hold-to-talk). Registered so getShortcutDisplay
  92|       // finds it without hitting the fallback analytics log. To rebind,
  93|       // add a voice:pushToTalk entry (last wins); to disable, use /voice
  94|       // — null-unbinding space hits a pre-existing useKeybinding.ts trap
  95|       // where 'unbound' swallows the event (space dead for typing).
  96|       ...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
  97|     },
  98|   },
  99|   {
 100|     context: 'Autocomplete',
 101|     bindings: {
 102|       tab: 'autocomplete:accept',
 103|       escape: 'autocomplete:dismiss',
 104|       up: 'autocomplete:previous',
 105|       down: 'autocomplete:next',
 106|     },
 107|   },
 108|   {
 109|     context: 'Settings',
 110|     bindings: {
 111|       // Settings menu uses escape only (not 'n') to dismiss
 112|       escape: 'confirm:no',
 113|       // Config panel list navigation (reuses Select actions)
 114|       up: 'select:previous',
 115|       down: 'select:next',
 116|       k: 'select:previous',
 117|       j: 'select:next',
 118|       'ctrl+p': 'select:previous',
 119|       'ctrl+n': 'select:next',
 120|       // Toggle/activate the selected setting (space only — enter saves & closes)
 121|       space: 'select:accept',
 122|       // Save and close the config panel
 123|       enter: 'settings:close',
 124|       // Enter search mode
 125|       '/': 'settings:search',
 126|       // Retry loading usage data (only active on error)
 127|       r: 'settings:retry',
 128|     },
 129|   },
 130|   {
 131|     context: 'Confirmation',
 132|     bindings: {
 133|       y: 'confirm:yes',
 134|       n: 'confirm:no',
 135|       enter: 'confirm:yes',
 136|       escape: 'confirm:no',
 137|       // Navigation for dialogs with lists
 138|       up: 'confirm:previous',
 139|       down: 'confirm:next',
 140|       tab: 'confirm:nextField',
 141|       space: 'confirm:toggle',
 142|       // Cycle modes (used in file permission dialogs and teams dialog)
 143|       'shift+tab': 'confirm:cycleMode',
 144|       // Toggle permission explanation in permission dialogs
 145|       'ctrl+e': 'confirm:toggleExplanation',
 146|       // Toggle permission debug info
 147|       'ctrl+d': 'permission:toggleDebug',
 148|     },
 149|   },

源码引用: src/hooks/useGlobalKeybindings.tsx · 第 1–80 行(共 265 行)

   1| /**
   2|  * Component that registers global keybinding handlers.
   3|  *
   4|  * Must be rendered inside KeybindingSetup to have access to the keybinding context.
   5|  * This component renders nothing - it just registers the keybinding handlers.
   6|  */
   7| import { feature } from 'bun:bundle'
   8| import { useCallback } from 'react'
   9| import instances from '../ink/instances.js'
  10| import { useKeybinding } from '../keybindings/useKeybinding.js'
  11| import type { Screen } from '../screens/REPL.js'
  12| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
  13| import {
  14|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  15|   logEvent,
  16| } from '../services/analytics/index.js'
  17| import { useAppState, useSetAppState } from '../state/AppState.js'
  18| import { count } from '../utils/array.js'
  19| import { getTerminalPanel } from '../utils/terminalPanel.js'
  20| 
  21| type Props = {
  22|   screen: Screen
  23|   setScreen: React.Dispatch<React.SetStateAction<Screen>>
  24|   showAllInTranscript: boolean
  25|   setShowAllInTranscript: React.Dispatch<React.SetStateAction<boolean>>
  26|   messageCount: number
  27|   onEnterTranscript?: () => void
  28|   onExitTranscript?: () => void
  29|   virtualScrollActive?: boolean
  30|   searchBarOpen?: boolean
  31| }
  32| 
  33| /**
  34|  * Registers global keybinding handlers for:
  35|  * - ctrl+t: Toggle todo list
  36|  * - ctrl+o: Toggle transcript mode
  37|  * - ctrl+e: Toggle showing all messages in transcript
  38|  * - ctrl+c/escape: Exit transcript mode
  39|  */
  40| export function GlobalKeybindingHandlers({
  41|   screen,
  42|   setScreen,
  43|   showAllInTranscript,
  44|   setShowAllInTranscript,
  45|   messageCount,
  46|   onEnterTranscript,
  47|   onExitTranscript,
  48|   virtualScrollActive,
  49|   searchBarOpen = false,
  50| }: Props): null {
  51|   const expandedView = useAppState(s => s.expandedView)
  52|   const setAppState = useSetAppState()
  53| 
  54|   // Toggle todo list (ctrl+t) - cycles through views
  55|   const handleToggleTodos = useCallback(() => {
  56|     logEvent('tengu_toggle_todos', {
  57|       is_expanded: expandedView === 'tasks',
  58|     })
  59|     setAppState(prev => {
  60|       const { getAllInProcessTeammateTasks } =
  61|         // eslint-disable-next-line @typescript-eslint/no-require-imports
  62|         require('../tasks/InProcessTeammateTask/InProcessTeammateTask.js') as typeof import('../tasks/InProcessTeammateTask/InProcessTeammateTask.js')
  63|       const hasTeammates =
  64|         count(
  65|           getAllInProcessTeammateTasks(prev.tasks),
  66|           t => t.status === 'running',
  67|         ) > 0
  68| 
  69|       if (hasTeammates) {
  70|         // Both exist: none → tasks → teammates → none
  71|         switch (prev.expandedView) {
  72|           case 'none':
  73|             return { ...prev, expandedView: 'tasks' as const }
  74|           case 'tasks':
  75|             return { ...prev, expandedView: 'teammates' as const }
  76|           case 'teammates':
  77|             return { ...prev, expandedView: 'none' as const }
  78|         }
  79|       }
  80|       // Only tasks: none ↔ tasks

源码引用: src/keybindings/reservedShortcuts.ts · 第 1–50 行(共 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
  41|  * - We use ctrl+s for the stash feature
  42|  */
  43| export const TERMINAL_RESERVED: ReservedShortcut[] = [
  44|   {
  45|     key: 'ctrl+z',
  46|     reason: 'Unix process suspend (SIGTSTP)',
  47|     severity: 'warning',
  48|   },
  49|   {
  50|     key: 'ctrl+\\',

阅读顺序建议

首次阅读 defaultBindings.ts 时,建议先看文件头平台分支,再看 Global 与 Chat 两个高频 context,最后看各类 dialog context。这样能先建立“全局动作与输入动作分层”的大图,再进入细节键位。

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

   7| /**
   8|  * Default keybindings that match current Claude Code behavior.
   9|  * These are loaded first, then user keybindings.json overrides them.
  10|  */
  11| 
  12| // Platform-specific image paste shortcut:
  13| // - Windows: alt+v (ctrl+v is system paste)
  14| // - Other platforms: ctrl+v
  15| const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'
  16| 
  17| // Modifier-only chords (like shift+tab) may fail on Windows Terminal without VT mode
  18| // See: https://github.com/microsoft/terminal/issues/879#issuecomment-618801651
  19| // Node enabled VT mode in 24.2.0 / 22.17.0: https://github.com/nodejs/node/pull/58358
  20| // Bun enabled VT mode in 1.2.23: https://github.com/oven-sh/bun/pull/21161
  21| const SUPPORTS_TERMINAL_VT_MODE =
  22|   getPlatform() !== 'windows' ||
  23|   (isRunningWithBun()
  24|     ? satisfies(process.versions.bun, '>=1.2.23')
  25|     : satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))
  26| 
  27| // Platform-specific mode cycle shortcut:
  28| // - Windows without VT mode: meta+m (shift+tab doesn't work reliably)
  29| // - Other platforms: shift+tab
  30| const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'
  31| 
  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|   },
  63|   {
  64|     context: 'Chat',
  65|     bindings: {
  66|       escape: 'chat:cancel',
  67|       // ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
  68|       'ctrl+x ctrl+k': 'chat:killAgents',
  69|       [MODE_CYCLE_KEY]: 'chat:cycleMode',
  70|       'meta+p': 'chat:modelPicker',
  71|       'meta+o': 'chat:fastMode',
  72|       'meta+t': 'chat:thinkingToggle',
  73|       enter: 'chat:submit',
  74|       up: 'history:previous',
  75|       down: 'history:next',
  76|       // Editing shortcuts (defined here, migration in progress)
  77|       // Undo has two bindings to support different terminal behaviors:
  78|       // - ctrl+_ for legacy terminals (send \x1f control char)
  79|       // - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
  80|       'ctrl+_': 'chat:undo',
  81|       'ctrl+shift+-': 'chat:undo',
  82|       // ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
  83|       'ctrl+x ctrl+e': 'chat:externalEditor',
  84|       'ctrl+g': 'chat:externalEditor',
  85|       'ctrl+s': 'chat:stash',
  86|       // Image paste shortcut (platform-specific key defined above)
  87|       [IMAGE_PASTE_KEY]: 'chat:imagePaste',
  88|       ...(feature('MESSAGE_ACTIONS')
  89|         ? { 'shift+up': 'chat:messageActions' as const }
  90|         : {}),
  91|       // Voice activation (hold-to-talk). Registered so getShortcutDisplay
  92|       // finds it without hitting the fallback analytics log. To rebind,
  93|       // add a voice:pushToTalk entry (last wins); to disable, use /voice
  94|       // — null-unbinding space hits a pre-existing useKeybinding.ts trap
  95|       // where 'unbound' swallows the event (space dead for typing).
  96|       ...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
  97|     },
  98|   },

本章小结与延伸

default-bindings 是产品默认 UX 的单一真相源。下一章 command-bindings 读 command:* 动态层。 继续学习:

  • command-bindings
  • keybinding-registry
Prev
keybinding-registry · 注册、Provider 与 useKeybinding
Next
command-bindings · command:* 动态斜杠命令绑定