Files
claude-code/packages/@ant/ink/docs/11-core-architecture.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

8.4 KiB

Chapter 11: Core Architecture

This chapter covers the internal rendering pipeline, DOM model, and screen buffer system. This is advanced material -- most users only need the component and hooks APIs.

Rendering Pipeline

React Component Tree
        ↓ (React reconciler)
Ink DOM Tree (virtual terminal DOM)
        ↓ (Yoga layout)
Positioned DOM Tree (computed x, y, width, height)
        ↓ (renderNodeToOutput)
Output Buffer (styled characters)
        ↓ (renderer → Screen)
Screen Buffer (Int32Array of cells)
        ↓ (diffEach)
ANSI Diff Patches (minimal escape sequences)
        ↓ (writeDiffToTerminal)
Terminal stdout

Frame Lifecycle

Each render cycle (onRender) follows these phases:

  1. React Commit -- React reconciles the virtual tree; host config updates Ink DOM
  2. Yoga Layout -- All dirty nodes have their styles applied and layout computed
  3. Renderer -- Creates Output buffer, calls renderNodeToOutput for the full tree
  4. Screen Diff -- New frame is compared against previous frame cell-by-cell
  5. Optimize -- Patches are merged and ordered for minimal cursor movement
  6. Write -- ANSI escape sequences are written to stdout

Frame Timing

const FRAME_INTERVAL_MS = 16  // ~60fps cap

Renders are throttled. Multiple state updates in one frame are batched.

Double Buffering

Two frames are maintained:

  • frontFrame -- The currently displayed frame
  • backFrame -- The frame being rendered

After rendering, they are swapped. This prevents partial updates from being visible.

Ink DOM

Node Types

type ElementNames =
  | 'ink-root'        // Root container
  | 'ink-box'         // Box component
  | 'ink-text'        // Text component
  | 'ink-virtual-text' // Intermediate text wrapper
  | 'ink-link'        // Link component
  | 'ink-raw-ansi'    // Raw ANSI content

DOMElement

type DOMElement = {
  nodeName: ElementNames
  attributes: Record<string, unknown>
  childNodes: DOMNode[]      // DOMElement | TextNode
  yogaNode?: LayoutNode      // Yoga layout node
  textStyles?: TextStyles    // Inherited text styles

  // Scroll state
  scrollTop?: number
  scrollHeight?: number
  scrollViewportHeight?: number
  scrollViewportTop?: number
  stickyScroll?: boolean
  pendingScrollDelta?: number
  scrollAnchor?: { el: DOMElement; offset: number }

  // Dirty tracking
  dirty: boolean

  // Event handlers (stored separately)
  onClick?: (event: ClickEvent) => void
  onFocus?: (event: FocusEvent) => void
  onBlur?: (event: FocusEvent) => void
  onKeyDown?: (event: KeyboardEvent) => void
  onMouseEnter?: () => void
  onMouseLeave?: () => void
}

TextNode

type TextNode = {
  nodeName: '#text'
  nodeValue: string
  yogaNode?: LayoutNode
}

DOM Operations

// Node creation
createNode(nodeName: string): DOMElement
createTextNode(text: string): TextNode

// Tree manipulation
appendChildNode(parent: DOMElement, child: DOMNode): void
insertBeforeNode(parent: DOMElement, child: DOMNode, before: DOMNode): void
removeChildNode(parent: DOMElement, child: DOMNode): void

// Attribute manipulation
setAttribute(node: DOMElement, key: string, value: unknown): void
setStyle(node: DOMElement, style: Styles): void
setTextStyles(node: DOMElement, styles: TextStyles): void

// Dirty tracking
markDirty(node: DOMElement): void
scheduleRenderFrom(node: DOMElement): void

Screen Buffer

Cell Storage

The screen buffer uses packed Int32Array storage for memory efficiency:

type Screen = {
  width: number
  height: number
  cells: Int32Array        // 2 Int32s per cell: [charId, packed_style_hyperlink_width]
  cells64: BigInt64Array   // For bulk fill operations
  charPool: CharPool       // String interning
  stylePool: StylePool     // ANSI code interning
  hyperlinkPool: HyperlinkPool
  emptyStyleId: number
  damage: Rectangle | undefined  // Bounding box of changed cells
  noSelect: Uint8Array     // Per-cell no-select bitmap
  softWrap: Int32Array     // Per-row soft-wrap markers
}

Cell Width

enum CellWidth {
  Narrow = 0,       // Regular character (1 column)
  Wide = 1,         // CJK/emoji (2 columns)
  SpacerTail = 2,   // Right half of wide character
  SpacerHead = 3,   // Soft-wrapped wide character
}

Style Pool

ANSI style codes are interned for efficiency:

class StylePool {
  intern(codes: AnsiCode[]): number    // Returns compact ID
  get(id: number): AnsiCode[]
  transition(from: number, to: number): string  // Cached ANSI transition
  withInverse(id: number): number     // Selection overlay
  setSelectionBg(bg: AnsiCode): void  // Theme-aware selection bg
}

Diff Algorithm

diffEach(prev: Screen, next: Screen, callback: (x, y, oldCell, newCell) => void): void

Only iterates cells within the damage bounding box. Unchanged regions are skipped entirely.

Screen Operations

createScreen(width, height, stylePool, charPool, hyperlinkPool): Screen
setCellAt(screen, x, y, cell): void
cellAt(screen, x, y): Cell
clearRegion(screen, x, y, width, height): void
blitRegion(dst, src, x, y, maxX, maxY): void
shiftRows(screen, top, bottom, n): void

Layout Engine

Yoga Integration

Ink wraps Facebook's Yoga layout engine for Flexbox computation:

// Layout node types
enum LayoutDisplay { Flex, None }
enum LayoutPositionType { Absolute, Relative }
enum LayoutOverflow { Visible, Hidden, Scroll }
enum LayoutFlexDirection { Row, Column, RowReverse, ColumnReverse }
enum LayoutWrap { NoWrap, Wrap, WrapReverse }
enum LayoutAlign { FlexStart, Center, FlexEnd, Stretch }
enum LayoutJustify { FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly }
enum LayoutEdge { Top, Bottom, Left, Right, Start, End, Horizontal, Vertical, All }
enum LayoutGutter { Column, Row, All }

Style Application

Styles from React props are applied to Yoga nodes during the commit phase:

function styles(node: LayoutNode, style: Styles, resolvedStyle?: Styles): void

This function maps each CSS-like prop to the corresponding Yoga setter.

Output Buffer

Intermediate rendering target before screen diff:

class Output {
  write(text: string, x: number, y: number, styles: TextStyles): void
  wrap(width: number, textWrap: TextWrap): void
}

renderNodeToOutput walks the DOM tree and writes styled characters into this buffer.

Reconciler

Custom React reconciler that bridges React and the Ink DOM:

  • Host config -- Defines how React operations map to Ink DOM mutations
  • Concurrent mode -- Supports ConcurrentRoot for React 19 features
  • Yoga integration -- Applies styles during commit phase
  • DevTools -- Connected in development mode

Host Config Methods

Method Purpose
createInstance Create ink-box, ink-text, etc.
createTextInstance Create #text node
appendChildNode Add child to parent
removeChildNode Remove child from parent
insertBefore Insert child before sibling
commitUpdate Update element attributes/styles
commitTextUpdate Update text content
getPublicInstance Return DOMElement for refs

Performance Optimizations

  1. String Interning -- CharPool deduplicates character strings across frames
  2. Style Caching -- StylePool caches ANSI transition strings
  3. Damage Tracking -- Only diff cells within the changed bounding box
  4. Bulk Operations -- Int32Array.set() for fast region blit
  5. Throttled Rendering -- Frame rate capped at ~60fps
  6. Viewport Culling -- ScrollBox only renders visible children
  7. Microtask Coalescing -- Multiple scroll deltas merged into one render

Frame Events

Debug instrumentation for render performance:

type FrameEvent = {
  durationMs: number
  phases: {
    renderer: number    // Yoga + renderNodeToOutput
    diff: number        // Screen diff
    optimize: number    // Patch optimization
    write: number       // Terminal write
    patches: number     // Number of ANSI patches
    yoga: number        // Yoga layout time
    commit: number      // React commit time
    yogaVisited: number // Yoga nodes visited
    yogaMeasured: number // Yoga nodes measured
    yogaCacheHits: number // Cached measurements
    yogaLive: number    // Active Yoga nodes
  }
  flickers: FlickerReason[]
}

Enable with onFrame in RenderOptions:

render(<App />, {
  onFrame: (event) => {
    console.log(`Frame: ${event.durationMs}ms`)
  }
})