How to Use OpenClaude's React/Ink Terminal UI: A Developer's Guide
OpenClaude renders its interactive command-line experience with React Ink, letting you build terminal UIs using React components that output ANSI-styled text.
The React/Ink terminal UI in OpenClaude powers everything from multi-line prompt editing to streaming message rendering and tool-specific visualizations. This guide walks through the component architecture, key implementation files, and practical ways to extend or embed the UI.
Understanding OpenClaude's React Ink Architecture
OpenClaude's UI layer follows a clean separation between the Ink runtime, React components, and tool-specific renderers. The system bootstraps in src/entrypoints/init.ts, which calls render(<App />) via the utility in src/utils/staticRender.tsx.
Core Layers
| Layer | Responsibility | Key Implementation |
|---|---|---|
| Ink runtime | Boots the render loop, creates root App component, provides useApp hook for graceful shutdown |
src/utils/staticRender.tsx |
| Root UI | Top-level layout, screen navigation (startup, prompt, spinner, results) | src/components/StartupScreen.tsx |
| Prompt input | Line editing, multi-line mode, auto-completion, floating hint icons | src/components/PromptInput/* |
| Message rendering | Streams assistant/tool output, "thinking" blocks, progressive updates | src/utils/exportRenderer.tsx |
| Tool UI | Per-tool Ink components formatting results in transcript | src/tools/*/UI.tsx |
| State & permissions | Central store for transcript, session metadata, confirmation dialogs | src/components/permissions/* |
| Visual helpers | Color palette, spinners, custom selects, hyperlinks | src/components/Spinner/*, src/components/CustomSelect/* |
The tools registry (implemented in src/entrypoints/sdk/*) passes down to all components, allowing any tool to render its own Ink UI. The useApp hook enables programmatic exit via app.exit() after completion or error.
Key Files for React Ink Development
These files form the backbone of OpenClaude's terminal UI:
src/utils/staticRender.tsx— Sets up Ink'srenderloop and captures stdout for testingsrc/components/StartupScreen.tsx— First view on launch, handles profile selectionsrc/components/PromptInput/*— Input handling withusePromptInputPlaceholder.tsandinputModes.tssrc/utils/exportRenderer.tsx— Streams React nodes as ANSI chunks for terminal displaysrc/tools/*/UI.tsx— Tool-specific fragments likeBashTool/UI.tsxandWebFetchTool/UI.tsxsrc/entrypoints/init.ts— CLI entry point that wires tool registry to Ink
Creating Custom Ink Components
You can embed custom React Ink components anywhere in OpenClaude's rendering pipeline.
Example: Custom Message Component
// src/components/MyMessage.tsx
import { Box, Text } from '../../ink.js';
import { useEffect } from 'react';
export default function MyMessage({ text }: { text: string }) {
useEffect(() => {
// Simulate a delayed thinking status
setTimeout(() => console.log('[thinking]'), 500);
}, []);
return (
<Box flexDirection="column" marginBottom={1}>
<Text color="cyan">💡 {text}</Text>
</Box>
);
}
Integrating into Tool Output
Use renderReactNode from the export renderer to display your component during tool execution:
// src/tools/CustomTool/CustomTool.tsx
import MyMessage from '../../components/MyMessage.js';
import { renderReactNode } from '../../utils/exportRenderer.js';
export async function runCustomTool(input: string) {
// Display custom UI during processing
await renderReactNode(<MyMessage text="Processing your request…" />);
// Continue with normal tool logic
const result = await processInput(input);
return result;
}
The renderReactNode utility in src/utils/exportRenderer.tsx handles streaming ANSI chunks to the terminal, ensuring your React component renders correctly within the existing transcript.
Running OpenClaude's React Ink UI Programmatically
Spawn the CLI to display the full interactive UI—including spinners, prompts, and tool results:
import { exec } from 'node:child_process';
// Launch OpenClaude with Ink terminal UI
exec('bun run openclaude --model=anthropic/claude-3-opus', (err, stdout, stderr) => {
if (err) {
console.error('CLI failed:', err);
return;
}
console.log('CLI finished. Output captured:');
console.log(stdout);
});
Since Ink renders directly to the terminal, the spawned process provides the complete interactive experience identical to manual invocation.
Customizing Visual Elements
Building a Custom Spinner
The spinner system uses useStalledAnimation for frame timing:
// src/components/Spinner/MySpinner.tsx
import { useStalledAnimation } from '../../Spinner/useStalledAnimation.js';
import { Box, Text } from '../../ink.js';
export function MySpinner() {
const { frames, index } = useStalledAnimation({ speed: 120 });
return (
<Box>
<Text>{frames[index]}</Text>
</Box>
);
}
To activate your custom spinner, export it as default in src/components/Spinner/index.ts:
// src/components/Spinner/index.ts
export { MySpinner as default } from './MySpinner.js';
Working with the Prompt Input System
The PromptInput directory (src/components/PromptInput/*) contains several specialized hooks and utilities:
usePromptInputPlaceholder.ts— Manages placeholder text and hint visibilityinputModes.ts— Defines single-line, multi-line, and auto-completion modes- Floating "fast-icon" hints — Contextual shortcuts displayed during input
These components leverage Ink's useInput hook for terminal key handling while maintaining React's component model.
Tool-Specific UI Patterns
Each tool in OpenClaude can expose its own Ink UI component. The convention places these in src/tools/{ToolName}/UI.tsx:
BashTool/UI.tsx— Formats shell command output with syntax highlightingWebFetchTool/UI.tsx— Renders fetched content with source attributionMCPTool/UI.tsx— Displays MCP server interactions
Tool UI components receive the tool's result data as props and return Ink elements that integrate into the transcript stream.
Summary
- OpenClaude's React Ink terminal UI is bootstrapped from
src/entrypoints/init.tsusingsrc/utils/staticRender.tsxas the Ink runtime entry point - Components live under
src/components/with specialized directories for PromptInput, Spinner, permissions, and CustomSelect - Tool-specific UIs follow the
src/tools/*/UI.tsxconvention and receive data via the tools registry insrc/entrypoints/sdk/* - Use
renderReactNodefromsrc/utils/exportRenderer.tsxto stream custom React components as ANSI output - The
useApphook provides programmatic control over CLI lifecycle including graceful exit
Frequently Asked Questions
How do I add a custom component to OpenClaude's terminal UI?
Create a React component using Ink's Box and Text elements in src/components/, then import and render it via renderReactNode from src/utils/exportRenderer.tsx. For tool-specific visuals, place the component in src/tools/{YourTool}/UI.tsx following the existing pattern.
Can I run OpenClaude without the interactive React Ink interface?
The CLI defaults to Ink rendering when invoked normally. For non-interactive or headless usage, you would need to modify the entry point in src/entrypoints/init.ts or spawn the process with output capture—though this limits functionality since many features depend on the interactive terminal UI.
What version of Ink does OpenClaude use?
OpenClaude imports from '../../ink.js' (and similar relative paths), indicating a bundled or vendored Ink distribution. Check package.json in the root of the repository for the exact semantic version, or examine src/utils/staticRender.tsx for any Ink-specific API usage that might indicate version requirements.
How do permission dialogs work in the React Ink UI?
Permission dialogs pause the render loop for user confirmation using components in src/components/permissions/*. The TrustDialog utilities in src/components/TrustDialog/utils.ts handle the visual presentation, while state management coordinates with useApp to block execution until the user responds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →