* 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>
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:
- Registered active contexts (most recent first)
- The hook's own
contextparameter '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'