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

303 lines
7.2 KiB
Markdown

# 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.
```tsx
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
```ts
type KeybindingsLoadResult = {
bindings: ParsedBinding[]
warnings: KeybindingWarning[]
}
```
### KeybindingWarning
```ts
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.
```tsx
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
```ts
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).
```tsx
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.
```tsx
// 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
```tsx
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:
```tsx
import {
parseKeystroke,
parseChord,
keystrokeToString,
chordToString,
keystrokeToDisplayString,
chordToDisplayString,
parseBindings,
} from '@anthropic/ink'
```
### `parseKeystroke(str)`
Parse a single keystroke string:
```ts
parseKeystroke('ctrl+shift+enter')
// → { key: 'enter', ctrl: true, alt: false, shift: true, meta: false, super: false }
```
### `parseChord(str)`
Parse a chord (space-separated keystrokes):
```ts
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:
```ts
parseBindings([
{
context: 'Global',
bindings: {
'ctrl+s': 'app:save',
'ctrl+k ctrl+s': 'app:saveAs',
}
}
])
// → ParsedBinding[]
```
## Match Functions
```tsx
import { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink'
```
### `getKeyName(input, key)`
Get the canonical key name from raw input:
```ts
getKeyName('\x1b[A', { upArrow: true }) // 'up'
```
### `matchesKeystroke(input, key, target)`
Check if raw input matches a parsed keystroke:
```ts
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
```tsx
import { resolveKey, resolveKeyWithChordState, getBindingDisplayText } from '@anthropic/ink'
```
### `resolveKey(input, key, contexts, bindings)`
Resolve input to a binding action:
```ts
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:
```ts
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:
```ts
getBindingDisplayText('app:save', 'Global', bindings) // 'Ctrl+S'
```