source dump of claude code
at main 340 lines 12 kB view raw
1import { feature } from 'bun:bundle' 2import { satisfies } from 'src/utils/semver.js' 3import { isRunningWithBun } from '../utils/bundledMode.js' 4import { getPlatform } from '../utils/platform.js' 5import type { KeybindingBlock } from './types.js' 6 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 15const 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 21const 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 30const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m' 31 32export 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 }, 150 { 151 context: 'Tabs', 152 bindings: { 153 // Tab cycling navigation 154 tab: 'tabs:next', 155 'shift+tab': 'tabs:previous', 156 right: 'tabs:next', 157 left: 'tabs:previous', 158 }, 159 }, 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) 221 { 222 context: 'Attachments', 223 bindings: { 224 right: 'attachments:next', 225 left: 'attachments:previous', 226 backspace: 'attachments:remove', 227 delete: 'attachments:remove', 228 down: 'attachments:exit', 229 escape: 'attachments:exit', 230 }, 231 }, 232 // Footer indicator navigation (tasks, teams, diff, loop) 233 { 234 context: 'Footer', 235 bindings: { 236 up: 'footer:up', 237 'ctrl+p': 'footer:up', 238 down: 'footer:down', 239 'ctrl+n': 'footer:down', 240 right: 'footer:next', 241 left: 'footer:previous', 242 enter: 'footer:openSelected', 243 escape: 'footer:clearSelection', 244 }, 245 }, 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]