InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

06-ink-rendering

Shared from "Claude-Code" on Inkdown

Ink Rendering

How Claude Code renders its terminal UI using a custom React renderer.


What Is Ink?

Ink is a custom React renderer that targets the terminal instead of the DOM. It lets you write terminal UIs using React components and JSX.

TypeScript
// This renders in the terminal, not in a browser
<Box flexDirection="column" padding={1}>
  <Text bold>Hello, Terminal!</Text>
  <Text color="gray">This is Ink.</Text>
</Box>
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Architecture Overview

Plain text
┌─────────────────────────────────────────────────────────┐
│                   React Components                       │
│              <Box>, <Text>, <Button>, etc.               │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                  React Reconciler                        │
│           (react-reconciler host config)                 │
│                                                          │
│  createInstance, appendChild, commitUpdate, etc.        │
│  Maps React elements to Ink DOM nodes                    │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                    Ink DOM                               │
│          DOM nodes with Yoga layout nodes                │
│     <ink-box>, <ink-text>, <ink-virtual-text>           │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                 Yoga Layout (WASM)                       │
│              Flexbox layout engine                       │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   Renderer                               │
│     Walks DOM tree → writes to Output buffer            │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                    Frame                                 │
│     Double-buffered screen (frontFrame, backFrame)       │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                    Diff                                  │
│     Compare backFrame vs frontFrame → patches            │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│              Terminal Output                             │
│     ANSI escape sequences → stdout                       │
└──────────────────────────────────────────────────────────┘

The Render Pipeline

1. React Update
TypeScript
ink.render(reactNode)
  → reconciler.updateContainerSync(node)
  → reconciler.flushSyncWork()

The user's component tree is wrapped in context providers:

  • App — stdin, keyboard, mouse, suspend/resume
  • ThemeProvider — theme context
  • TerminalSizeContext — terminal dimensions
  • TerminalFocusContext — focus state
2. Reconciler Commit

After React commits, resetAfterCommit fires:

TypeScript
resetAfterCommit(rootNode) {
  rootNode.onComputeLayout()  // Yoga layout
  rootNode.onRender()         // Throttled paint cycle
}
3. Layout (Yoga)

Yoga is a flexbox layout engine compiled to WASM. Every Ink DOM node has an associated Yoga node:

TypeScript
// When creating an element:
const yogaNode = Yoga.Node.create()
Yoga.Node.setStyle(yogaNode, Yoga.PROPERTY_FLEX_DIRECTION, Yoga.FLEX_DIRECTION_COLUMN)
Yoga.Node.setStyle(yogaNode, Yoga.PROPERTY_PADDING, 10)

// When computing layout:
Yoga.Node.calculateLayout(rootYogaNode, terminalWidth, undefined, Yoga.DIRECTION_LTR)
4. Paint (Renderer)

The renderer walks the DOM tree and writes to an Output buffer:

TypeScript
function renderer() {
  // Validate dimensions
  if (!width || !height) return emptyFrame

  // Allocate screen
  const screen = backFrame.screen ?? Screen.create(width, height)
  const output = new Output(width, height)

  // Walk tree
  renderNodeToOutput(rootNode, output, { prevScreen: frontFrame.screen })

  // Build frame
  return new Frame(screen, width, height, cursorPosition)
}

Blit optimization: If a subtree hasn't changed, it's bulk-copied from the previous frame's screen buffer instead of re-rendering children:

TypeScript
// In renderNodeToOutput:
if (prevScreen && !nodeDirty) {
  output.blit(prevScreen, x, y, width, height)  // Fast memcpy
  return
}
// Otherwise, render children recursively
5. Diff

The new frame is diffed against the previous frame:

TypeScript
diffEach(prevScreen, nextScreen, (x, y, oldCell, newCell) => {
  // Record the change
})

The diff algorithm:

  • Uses packed Int32Array cells (2 words per cell)
  • Scans for differences with raw integer comparison
  • Only iterates the damage union (changed regions)
  • No object allocation during diff
6. Terminal Output

The diff produces terminal escape sequences:

Plain text
\x1b[H          — Cursor home (alt-screen anchoring)
\x1b[2J         — Clear screen
\x1b[31mHello   — Red text
\x1b[0m         — Reset
\x1b[10;20H     — Move cursor to row 10, column 20

Key behaviors:

  • Relative cursor moves: All positioning is relative to previous cursor position
  • Alt-screen anchoring: Every alt-screen frame prepends cursor home to prevent drift
  • Cursor parking: Cursor is parked at declared position (for IME input)

Screen Buffer

The Screen is the pixel buffer — a packed Int32Array:

Plain text
Each cell = 2 words (8 bytes):
  Word 0: charId (index into CharPool)
  Word 1: styleId[31:17] | hyperlinkId[16:2] | width[1:0]

This packed layout halves memory accesses and enables future SIMD comparison.

Style Pool

Interns ANSI style arrays into integer IDs:

TypeScript
// Instead of storing ["\x1b[31m", "\x1b[1m"] per cell,
// store a single integer ID that maps to those styles
const styleId = StylePool.intern(["\x1b[31m", "\x1b[1m"])

Features:

  • Cached style transitions (transition(fromId, toId)) for zero-allocation ANSI strings
  • Inverse, selection-bg, and current-match style variants
  • Bit 0 encodes visibility on spaces
Char Pool

Interns character strings with an ASCII fast-path:

TypeScript
// ASCII characters (code < 128): direct Int32Array lookup
// Non-ASCII: interned string with ID
const charId = CharPool.intern("A")  // Fast path: returns 65

Key Components

Layout Primitives
<Box>

The fundamental layout primitive — equivalent to <div style="display:flex">:

TypeScript
<Box flexDirection="column" padding={1} gap={1}>
  <Text>Hello</Text>
  <Text>World</Text>
</Box>

Supports: flex direction, grow, shrink, wrap, margins, padding, gaps, overflow, event handlers (click, focus, hover, keydown).

<Text>

Displays styled text:

TypeScript
<Text bold color="red" wrap="truncate-end">
  Hello World
</Text>

Supports: color, background, bold, dim, italic, underline, strikethrough, inverse, text wrapping modes.

<ScrollBox>

A Box with overflow scroll and imperative scroll API:

TypeScript
const scrollRef = useRef<ScrollBoxHandle>(null)
<ScrollBox ref={scrollRef}>
  {longContent}
</ScrollBox>

Features: viewport culling (only renders visible children), sticky scroll (auto-follow), scrollTo/scrollBy/scrollToBottom.

Interactive Components
ComponentPurpose
<Button>Clickable button with state management
<Link>Clickable link (OSC 8 hyperlinks)
<AlternateScreen>Fullscreen mode (alternate screen buffer)
<NoSelect>Non-selectable region (gutters, line numbers)
Utility Components
ComponentPurpose
<Spacer>Flexible spacing
<Newline>Insert newline(s)
<RawAnsi>Render raw ANSI escape sequences
<ErrorOverview>Error boundary display
Root Component (<App>)

The root component that wraps everything:

  • Manages stdin raw mode
  • Parses keyboard input (including escape sequences)
  • Handles mouse events
  • Manages terminal focus
  • Handles suspend/resume (SIGSTOP/SIGCONT)
  • Detects multi-click
  • Handles hyperlinks

Input Handling

Keyboard

The <App> component parses keyboard input:

TypeScript
// Key events are dispatched through the reconciler
dispatcher.dispatchDiscrete(() => {
  // All state updates from this input burst are batched
})

Special handling:

  • Escape sequences (arrow keys, function keys)
  • Ctrl/Alt/Shift combinations
  • Paste detection
  • Bracketed paste mode
Mouse

Mouse events are tracked in <AlternateScreen> mode:

  • Click, double-click, triple-click detection
  • Mouse drag
  • Scroll wheel
  • Hit-testing for clickable elements

Throttling & Performance

Frame Rate

Rendering is throttled to ~60fps (16ms frame interval):

TypeScript
const onRender = throttle(() => {
  const frame = renderer()
  const patches = diff(frontFrame, frame)
  writePatchesToTerminal(patches)
  frontFrame = frame
}, FRAME_INTERVAL_MS)
Resource Pools

Shared resource pools avoid per-frame allocations:

  • StylePool — interned style IDs
  • CharPool — interned character IDs
  • HyperlinkPool — interned hyperlink IDs
Viewport Culling

<ScrollBox> only renders children in the visible window, not the entire scrollable content.


Terminal Management

Resize Handling
TypeScript
process.stdout.on('resize', () => {
  // Recalculate layout with new dimensions
  // Re-render with new terminal size
})
Suspend/Resume
TypeScript
process.on('SIGTSTP', () => {
  // Exit alt screen, restore terminal
})
process.on('SIGCONT', () => {
  // Re-enter alt screen, re-render
})
Alt Screen

The <AlternateScreen> component:

  • Enters alternate screen buffer (DEC 1049)
  • Enables mouse tracking
  • Constrains height to terminal rows
  • On unmount: exits alt screen, restores main screen

Public API (ink.ts)

TypeScript
// Render a component tree
render(node: ReactElement): { unmount(): void }

// Create a root for incremental rendering
createRoot(container: Element): { render(node: ReactElement): void }

// Components
export { Box, Text, Button, Link, ScrollBox, Spacer, Newline, ... }

// Hooks
export { useInput, useApp, useStdin, useInterval, useAnimationFrame, ... }

// Theme
export { ThemeProvider, useTheme, useThemeSetting, color }

Key Files Reference

FilePurpose
src/ink.tsPublic API (render, createRoot, exports)
src/ink/ink.tsxInk class — central orchestrator
src/ink/reconciler.tsReact reconciler host config
src/ink/renderer.tsTree walk → Output buffer
src/ink/output.tsOperation recorder (write, blit, clear, clip)
src/ink/screen.tsPixel buffer (Int32Array), StylePool, CharPool
src/ink/log-update.tsDiff → terminal escape sequences
src/ink/root.tsRoot component management
src/ink/components/App.tsxRoot app component (input, suspend, mouse)
src/ink/components/Box.tsxLayout primitive
src/ink/components/Text.tsxText display
src/ink/components/ScrollBox.tsxScrollable container
src/ink/components/Button.tsxInteractive button
src/ink/components/AlternateScreen.tsxFullscreen mode