* 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>
4.9 KiB
Chapter 10: Events & Focus
Event System
Ink implements a DOM-like event system with capture/bubble phases, propagation control, and prioritized dispatch.
Event Classes
All events extend the base Event class:
class Event {
stopImmediatePropagation(): void
}
InputEvent
Emitted for every keystroke or input action.
class InputEvent extends Event {
readonly input: string // Character(s) entered
readonly key: Key // Parsed key metadata
readonly keypress: ParsedKey // Raw keypress data
}
KeyboardEvent
DOM-like keyboard event for focused elements.
class KeyboardEvent extends Event {
readonly key: Key
}
Dispatched via onKeyDown / onKeyDownCapture on Box.
ClickEvent
Mouse click event (alt-screen only).
class ClickEvent extends Event {
readonly x: number // Column (0-indexed)
readonly y: number // Row (0-indexed)
}
Clicks bubble from the deepest hit Box up through ancestors.
FocusEvent
Focus change event.
class FocusEvent extends Event {
readonly relatedTarget: DOMElement | null
}
TerminalFocusEvent
Terminal window focus change.
class TerminalFocusEvent extends Event {
readonly type: 'terminalfocus' | 'terminalblur'
}
ResizeEvent
Terminal resize event (internal).
PasteEvent
Pasted text event (bracketed paste mode).
Event Dispatch Flow
stdin data → parse-keypress → InputEvent
↓
App.handleInput (useInput handlers)
↓
Box.onKeyDown (focused element, bubble)
Capture and Bubble Phases
<Box
onKeyDownCapture={(e) => {
// Capture phase: fires top-down
console.log('Parent captures key')
}}
onKeyDown={(e) => {
// Bubble phase: fires bottom-up
console.log('Parent receives bubbled key')
}}
>
<Box
onKeyDown={(e) => {
// Target: fires first in bubble phase
console.log('Child handles key')
e.stopImmediatePropagation() // Stop here
}}
>
<Text>Focus here</Text>
</Box>
</Box>
Event Propagation Methods
| Method | Effect |
|---|---|
event.stopImmediatePropagation() |
Stop all subsequent handlers |
event.preventDefault() |
Not supported in terminal context |
FocusManager
DOM-like focus management system.
How Focus Works
- Elements with
tabIndex >= 0participate in Tab/Shift+Tab cycling - Elements with
tabIndex === -1are programmatically focusable only - Elements with
autoFocusreceive focus on mount - Clicking a focusable element focuses it
Focus API
class FocusManager {
activeElement: DOMElement | null
focus(node: DOMElement): void
blur(): void
focusNext(root: DOMElement): void // Tab
focusPrevious(root: DOMElement): void // Shift+Tab
handleNodeRemoved(node: DOMElement, root: DOMElement): void
handleAutoFocus(node: DOMElement): void
handleClickFocus(node: DOMElement): void
enable(): void
disable(): void
}
Tab Navigation
<Box flexDirection="column">
<Button tabIndex={0} onAction={handleSave}>
{(s) => <Text>{s.focused ? '> Save' : ' Save'}</Text>}
</Button>
<Button tabIndex={0} onAction={handleCancel}>
{(s) => <Text>{s.focused ? '> Cancel' : ' Cancel'}</Text>}
</Button>
<Button tabIndex={-1} onAction={handleSecret}>
{/* Not reachable via Tab */}
{(s) => <Text>Secret</Text>}
</Button>
</Box>
Auto Focus
<Box tabIndex={0} autoFocus onKeyDown={handleKey}>
<Text>Receives focus immediately on mount</Text>
</Box>
Focus Events
<Box
tabIndex={0}
onFocus={(e) => console.log('Got focus')}
onBlur={(e) => console.log('Lost focus')}
onFocusCapture={(e) => console.log('Capture: focus in')}
onBlurCapture={(e) => console.log('Capture: focus out')}
>
<Text>Focusable element</Text>
</Box>
Hit Testing
Mouse click/hover resolution:
- Screen coordinates are mapped to DOM elements via Yoga layout
- The deepest element at the click position is the target
- Click events bubble upward through ancestors
- Hover events use
mouseenter/mouseleavesemantics (no bubbling between children)
Click Hit Testing
dispatchClick(rootNode, col, row): void
Walks the DOM tree, finds the deepest Box at (col, row), fires onClick, then bubbles to ancestors.
Hover Hit Testing
dispatchHover(rootNode, col, row, hoveredNodes): void
Tracks which nodes are under the pointer. Fires onMouseEnter/onMouseLeave as the pointer moves between elements.
EventEmitter
Custom event emitter for internal use:
class EventEmitter {
on(event: string, handler: Function): void
off(event: string, handler: Function): void
emit(event: string, ...args: any[]): void
removeListener(event: string, handler: Function): void
}
Used internally by the Ink instance for input events.