mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
* 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>
303 lines
7.2 KiB
Markdown
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'
|
|
```
|