Files
claude-code/packages/@ant/ink/docs/08-keybindings.md
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

7.2 KiB

Chapter 8: Keybinding System

The keybinding system provides configurable, context-aware keyboard shortcuts with chord sequence support.

Architecture

KeybindingSetup (loads config)
  └── KeybindingProvider (provides context)
        ├── useKeybinding(action, handler)
        ├── useKeybindings({ action: handler })
        ├── useKeybindingContext()
        └── useRegisterKeybindingContext(name, isActive)

KeybindingSetup

Loads and validates keybinding configuration at app startup.

import { KeybindingSetup } from '@anthropic/ink'

<KeybindingSetup
  loadBindings={() => parseUserKeybindings(configFile)}
  subscribeToChanges={(cb) => watchConfigFile(cb)}
  onWarnings={(warnings, isReload) => {
    warnings.forEach(w => console.warn(w.message))
  }}
>
  <App />
</KeybindingSetup>

Props

Prop Type Description
children ReactNode App tree
loadBindings () => KeybindingsLoadResult Load bindings from config
subscribeToChanges (cb) => unsubscribe Watch for config changes
initWatcher () => void | Promise<void> One-time setup (optional)
onWarnings (warnings, isReload) => void Validation warnings (optional)
onDebugLog (message) => void Debug logging (optional)

KeybindingsLoadResult

type KeybindingsLoadResult = {
  bindings: ParsedBinding[]
  warnings: KeybindingWarning[]
}

KeybindingWarning

type KeybindingWarning = {
  type: 'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'
  severity: 'error' | 'warning'
  message: string
  key?: string
  context?: string
  action?: string
  suggestion?: string
}

KeybindingProvider

Context provider that holds binding state and resolution logic. Automatically provided by KeybindingSetup.

useKeybinding

Register a handler for a keybinding action.

import { useKeybinding } from '@anthropic/ink'

function MyComponent() {
  useKeybinding('app:toggleTodos', () => {
    setShowTodos(prev => !prev)
  }, { context: 'Global' })

  // Return false to NOT consume the event (allow propagation)
  useKeybinding('scroll:lineDown', () => {
    if (!hasContent) return false  // Don't consume
    scrollBy(1)
  })
}

Signature

function useKeybinding(
  action: string,
  handler: () => void | false | Promise<void>,
  options?: { context?: string; isActive?: boolean }
): void

Handler Return Values

Return Effect
undefined / void Event consumed, stop propagation
false Event NOT consumed, propagate to other handlers
Promise<void> Async handler, treated as consumed

useKeybindings

Register multiple handlers in one hook (reduces useInput overhead).

import { useKeybindings } from '@anthropic/ink'

useKeybindings({
  'chat:submit': () => handleSubmit(),
  'chat:cancel': () => handleCancel(),
  'scroll:pageDown': () => {
    scrollBy(viewportHeight)
  },
  'scroll:lineDown': () => {
    if (!hasContent) return false
    scrollBy(1)
  },
}, { context: 'Chat' })

Keybinding Contexts

Contexts allow the same key to perform different actions depending on what's active.

// Register a context as active
import { useRegisterKeybindingContext } from '@anthropic/ink'

function ThemePicker({ isOpen }) {
  useRegisterKeybindingContext('ThemePicker', isOpen)

  // While open, 'ThemePicker' context bindings take precedence
  useKeybinding('picker:select', handleSelect, { context: 'ThemePicker' })

  return isOpen ? <PickerUI /> : null
}

Context resolution order:

  1. Registered active contexts (most recent first)
  2. The hook's own context parameter
  3. 'Global' (always checked last)

Chord Sequences

Keybindings support multi-key sequences (chords):

"ctrl+k ctrl+s"  →  Save (press Ctrl+K, then Ctrl+S)
"ctrl+k ctrl+c"  →  Close (press Ctrl+K, then Ctrl+C)

When a chord prefix is pressed:

  • result.type === 'chord_started' -- Show "Ctrl+K ..." pending indicator
  • Next key completes or cancels the chord
  • result.type === 'chord_cancelled' -- Invalid key, reset

KeybindingContext Hook

import { useKeybindingContext, useOptionalKeybindingContext } from '@anthropic/ink'

const ctx = useKeybindingContext()
// ctx.resolve(input, key, contexts)  → ResolveResult
// ctx.bindings                       → ParsedBinding[]
// ctx.pendingChord                   → ParsedKeystroke[] | null
// ctx.activeContexts                  → Set<string>
// ctx.getDisplayText(action, context) → string | undefined
// ctx.invokeAction(action)           → boolean
// ctx.registerHandler(registration)   → () => void  (unsubscribe)

// Returns null outside provider (no throw)
const optionalCtx = useOptionalKeybindingContext()

Parser Functions

Parse and format keybinding strings:

import {
  parseKeystroke,
  parseChord,
  keystrokeToString,
  chordToString,
  keystrokeToDisplayString,
  chordToDisplayString,
  parseBindings,
} from '@anthropic/ink'

parseKeystroke(str)

Parse a single keystroke string:

parseKeystroke('ctrl+shift+enter')
// → { key: 'enter', ctrl: true, alt: false, shift: true, meta: false, super: false }

parseChord(str)

Parse a chord (space-separated keystrokes):

parseChord('ctrl+k ctrl+s')
// → [{ key: 'k', ctrl: true, ... }, { key: 's', ctrl: true, ... }]

keystrokeToString(ks) / chordToString(chord)

Convert parsed keystroke/chord back to string.

keystrokeToDisplayString(ks) / chordToDisplayString(chord)

Convert to human-readable display string (platform-aware).

parseBindings(blocks)

Parse a keybinding configuration:

parseBindings([
  {
    context: 'Global',
    bindings: {
      'ctrl+s': 'app:save',
      'ctrl+k ctrl+s': 'app:saveAs',
    }
  }
])
// → ParsedBinding[]

Match Functions

import { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink'

getKeyName(input, key)

Get the canonical key name from raw input:

getKeyName('\x1b[A', { upArrow: true })  // 'up'

matchesKeystroke(input, key, target)

Check if raw input matches a parsed keystroke:

matchesKeystroke('s', { ctrl: true, shift: false }, { key: 's', ctrl: true })

matchesBinding(input, key, binding)

Check if raw input matches any keystroke in a binding's chord.

Resolver Functions

import { resolveKey, resolveKeyWithChordState, getBindingDisplayText } from '@anthropic/ink'

resolveKey(input, key, contexts, bindings)

Resolve input to a binding action:

const result = resolveKey('s', { ctrl: true, shift: false }, ['Global'], bindings)
// result.type: 'match' | 'none' | 'unbound'
// result.action: string (when type === 'match')

resolveKeyWithChordState(input, key, contexts, bindings, pendingChord)

Resolve with chord state:

const result = resolveKeyWithChordState('k', key, ['Global'], bindings, null)
// result.type: 'match' | 'none' | 'unbound' | 'chord_started' | 'chord_cancelled'
// result.pending: ParsedKeystroke[] (when type === 'chord_started')

getBindingDisplayText(action, context, bindings)

Get the display string for a binding:

getBindingDisplayText('app:save', 'Global', bindings)  // 'Ctrl+S'