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:


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 visibility
  • inputModes.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:

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.ts using src/utils/staticRender.tsx as 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.tsx convention and receive data via the tools registry in src/entrypoints/sdk/*
  • Use renderReactNode from src/utils/exportRenderer.tsx to stream custom React components as ANSI output
  • The useApp hook 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →