Files
claude-code/packages/@ant/ink/docs/10-events-and-focus.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

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

  1. Elements with tabIndex >= 0 participate in Tab/Shift+Tab cycling
  2. Elements with tabIndex === -1 are programmatically focusable only
  3. Elements with autoFocus receive focus on mount
  4. 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:

  1. Screen coordinates are mapped to DOM elements via Yoga layout
  2. The deepest element at the click position is the target
  3. Click events bubble upward through ancestors
  4. Hover events use mouseenter/mouseleave semantics (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.