A React App Inside Your Terminal
Claude Code’s terminal interface is not a traditional line-by-line CLI output. It’s a full React application. Using Ink — a React renderer for terminals — it delivers componentized UI, Flexbox layout, double-buffered rendering, and interactive dialogs. Choosing Ink over traditional libraries like ncurses or blessed came down to one thing: tapping the React ecosystem. Component-based architecture, state management, lifecycle hooks, and developer familiarity — all of it transfers directly.
Ink’s architecture has four layers. React Components get converted into Ink Host Config calls through a custom React Reconciler. The Host Config uses the Yoga layout engine to compute Flexbox layouts in terminal character units. The Terminal Renderer then converts those layout results into ANSI escape sequences written to stdout. The result: terminal UI development feels close to web frontend work, but you keep terminal-grade performance.
Core Components and Layout
The component tree follows a typical GUI application pattern. App serves as the root component, containing Header (title and status indicators), Main (message list, toolbar, context panel), and Footer (input box and suggestions). Within the message list, AssistantMessage nests ToolCall and ToolResult child components, forming a tree-structured message hierarchy.
The layout system is powered by the Yoga engine with full Flexbox support: flexDirection, justifyContent, alignItems, flexGrow, padding, margin, borderStyle, and more. One key difference from the browser environment is the unit of measurement — the terminal uses characters, not pixels. A padding: 1 means one character of padding on each side.
1 | // src/components/App.tsx |
Text measurement is the foundation here. The Yoga engine needs to know every text segment’s width and height to lay things out correctly. The measurement process handles three issues: ANSI escape sequences don’t count toward width (they’re control characters that take no display space), multi-line text requires per-line calculation, and CJK characters occupy 2 columns (full-width characters).
1 | function measureText(text: string): { width: number; height: number } { |
Double-Buffered Rendering
Double buffering is the key technique for avoiding terminal flicker. The rendering pipeline has five steps: a state update triggers the Reconciler to update the Yoga Tree, the Layout engine recalculates positions, the frame renders into Buffer A (producing ANSI sequences), buffers swap (Buffer A becomes Previous Frame, Buffer B becomes Current Frame), and a diff between the two frames produces output only for changed regions.
The diff algorithm compares Previous and Current frames line by line. Only lines with actual content changes generate ANSI cursor movement and write instructions. This design reduces each frame’s output from the entire screen to just the changed lines, cutting terminal I/O significantly.
1 | function diffScreens(prev: string[], curr: string[]): DiffOutput[] { |
Rendering also has a throttle mechanism with a 16ms interval (roughly 60fps). Multiple state updates within the same render interval get merged into a single render pass, avoiding excessive terminal refreshes.
Interactive Components
Terminal interaction relies on the useInput Hook to capture keyboard events. The InputBox component handles character input, cursor movement (left/right arrows), backspace deletion, and enter-to-submit. It maintains cursor position and text content through string concatenation.
1 | function InputBox({ onSubmit }) { |
The SelectMenu component uses up/down arrows to navigate an option list with enter to confirm. The PermissionDialog component uses left/right arrows to toggle between Allow and Deny, with enter to submit. The common pattern across these components: useState manages selection state, useInput captures keyboard events, and conditional rendering highlights the currently selected item.
Tool Call Visualization
The ToolCall component displays different colored borders and icons based on tool execution status (pending/running/success/error). Running state shows an input parameter summary, success shows elapsed time, and error displays the error message. The ToolResult component truncates long output (default 500 characters), and users can press enter to expand the full content.
1 | function ToolCall({ toolName, input, status }) { |
This state-driven visual design lets users perceive the progress and result of each tool call at a glance, without reading raw logs.
Virtual Scrolling for Long Lists
When the conversation history grows long, the message list can contain hundreds of entries. The virtual scrolling component renders only the items in the visible region, using scrollTop state to control the visible window. Up/down arrow keys update scrollTop and recalculate which items are visible.
1 | function VirtualList({ items, height }) { |
Virtual scrolling drops rendering cost from O(n) to O(visible), eliminating terminal refresh stutter in long conversation scenarios.
Non-Interactive Mode
Claude Code automatically detects its runtime environment. In non-TTY environments (piped output, CI, --non-interactive flag), it switches to a simplified output mode. Detection checks whether stdout is a TTY, whether the CI=true environment variable is set, and whether the --non-interactive flag is present.
In non-interactive mode, messages output as plain text: user messages get prefixed with >, assistant messages output directly, tool calls show [Tool: name] with input parameters, tool results show the first 500 characters, and errors display as [Error] message. This degradation ensures Claude Code works as a pipe command (echo "fix bug" | claude) where output needs to be parseable by other tools.
Colors and Style System
The color system defines a unified semantic mapping: cyan for user messages, white for assistant messages, four colors for tool status (pending yellow, running blue, success green, error red), and status indicators using green for active, gray for idle, red for error. These colors are implemented through ANSI escape sequences with support for bold, dim, and other text modifiers.
Border styles come in three variants: single (single line), double (double line), and rounded (rounded corners). The permission dialog uses double-line borders with a yellow theme, visually separating it from regular components and emphasizing its importance.
Key Source Files
| File | Responsibility |
|---|---|
src/components/App.tsx |
Main application entry point |
src/components/Header.tsx |
Title bar and status indicators |
src/components/MessageList.tsx |
Message list rendering |
src/components/ToolCall.tsx |
Tool call visualization |
src/components/InputBox.tsx |
Input box component |
src/components/PermissionDialog.tsx |
Permission dialog |
src/renderers/nonInteractive.ts |
Non-interactive mode rendering |
src/styles/colors.ts |
Color system |
ink/lib/renderer.ts |
Double-buffered rendering engine |
ink/lib/measureText.ts |
Text measurement |
Series Navigation:
- Previous: Computer Use: Nine Layers of Security for Desktop Control
- End of series