A high-performance terminal renderer for AI agent applications. A React Ink alternative built for streaming, persistence, and replay.
If you're building an LLM-powered CLI tool — streaming responses, tool execution, permission prompts — React Ink starts to hurt:
- Full-tree re-render on every commit: Each state update triggers a React reconciliation pass, Yoga layout recalculation, and a recursive walk of the entire node tree to rebuild the output buffer. Ink throttles terminal writes to 30 FPS, but the tree walk and buffer rebuild happen on every React commit.
- No scrollable viewport: Ink manages cursor position, but has no built-in scrolling. When content exceeds terminal height, rendering breaks (ink#359). Historical output from
<Static>is pushed to terminal scrollback with no way to navigate it programmatically. - No persistence: Close the terminal, lose the session. There's no built-in way to save or restore state.
- No replay: Can't record, inspect, or debug past sessions.
Camouflage takes a different approach:
| Concern | React Ink | Camouflage |
|---|---|---|
| State model | retained React component tree | append-only event log + bounded view |
| Rendering | full tree walk + buffer rebuild per React commit (output throttled to 30 FPS) | mutates one active row in place, 60 FPS |
| Memory | application must manage its own transcript state | bounded (2000-row cap, older rows paged from SQLite) |
| Viewport | no built-in scrolling; overflows break rendering | own viewport with scroll, search, bookmarks |
| Persistence | none | SQLite WAL, every event persisted before render |
| Replay | not supported | first-class, deterministic from event log |
npm install camouflage-tuiPre-built binaries are downloaded automatically for macOS and Linux. No Rust toolchain required.
import { mount } from "camouflage-tui";
const cam = await mount();
// Start a session
cam.send("SessionStarted", {});
// Show a user message
cam.send("UserMessageCreated", { text: "What files changed?" });
// Stream an assistant response
cam.send("AssistantStreamStarted", { stream_id: "s1" });
cam.send("AssistantTokenDelta", { stream_id: "s1", token: "Let me " });
cam.send("AssistantTokenDelta", { stream_id: "s1", token: "check..." });
cam.send("AssistantMessageCompleted", { stream_id: "s1" });
// Listen for user input
cam.on("userInput", (text) => {
console.log("user said:", text);
});
await cam.close();Camouflage is event-driven, not component-driven. Your application emits NDJSON events; Camouflage persists, renders, and manages the terminal. You never touch the terminal directly.
Your app (Node/Python/Rust/Go)
│ emit NDJSON events
▼
camouflage-tui (Rust binary)
├─ Persists to SQLite (append-only, WAL)
├─ Renders to terminal (ratatui, 60 FPS)
└─ Sends user responses back to your app
This means:
- Any language can drive the TUI — just emit JSON lines
- Process isolation — renderer crash doesn't kill your app
- Deterministic replay — replay any session from the event log
No need to build UI from scratch. Camouflage includes:
| Component | What it does |
|---|---|
| Transcript | Streaming chat with user/assistant/tool/system rows |
| Status bar | Configurable segments (mode, phase, tokens, cost, etc.) |
| Task ribbon | Background task progress indicators |
| Permission widget | Inline approve/deny modal with feedback |
| SelectList | Filterable dropdown picker |
| Confirm | Yes/no dialog |
| Form | Multi-field text/password input |
| Table | Tabular data display |
| Wizard | Multi-step flow composing select/confirm/form steps |
| Diff viewer | Unified-diff rendering with syntax coloring |
Each component is triggered by sending an event and (for interactive ones) listening for the response:
import { selectList, confirm, form } from "camouflage-tui";
const model = await selectList(cam, {
id: "model-picker",
prompt: "Choose a model",
options: [
{ value: "gpt-4", label: "GPT-4" },
{ value: "claude", label: "Claude" },
],
});
const confirmed = await confirm(cam, {
id: "deploy",
prompt: "Deploy to production?",
});Camouflage uses a simple NDJSON wire protocol with 40+ event types. The minimum:
{"event_type":"UserMessageCreated","payload":{"text":"hello"}}
{"event_type":"AssistantStreamStarted","payload":{"stream_id":"s1"}}
{"event_type":"AssistantTokenDelta","payload":{"stream_id":"s1","token":"hi"}}
{"event_type":"AssistantMessageCompleted","payload":{"stream_id":"s1"}}Fields like id, session_id, seq, and timestamp_ms are auto-filled if absent.
See docs/protocol.md for the full reference.
7 built-in themes, switchable at runtime with the T key:
- default-dark, default-light
- dracula, nord, gruvbox-dark, tokyo-night
Themes use 24-bit RGB semantic colors (accent, user, assistant, tool, error, diff colors, etc.).
| Key | Action |
|---|---|
| Enter | Submit input |
| Esc | Cancel active stream |
| Up/Down | Scroll line by line |
| PgUp/PgDn | Scroll page |
| End / Ctrl+E | Jump to latest, re-engage auto-follow |
| T | Cycle theme |
| ? | Help overlay |
| Ctrl+F | Search |
| / | Slash command picker |
| @ | Mention picker |
| r | Replay current session |
| q / Ctrl+C | Quit |
Every event is persisted to SQLite before rendering. Sessions can be replayed deterministically:
camouflage-tui --replay <SESSION_UUID>Replay mode includes play/pause (Space), step forward/backward (arrows), speed control (+/-), and timeline jumping (1-9 for 10%-90%).
See ARCHITECTURE.md for the full design. The workspace contains:
| Crate | What |
|---|---|
protocol |
Event schema (serde types) |
store |
SQLite WAL persistence |
renderer |
Pure render logic (RenderModel + Snapshot) |
tui |
Terminal UI (ratatui + crossterm) |
headless |
Non-TUI tools (record, broadcast, export, validate) |
sdk |
Rust SDK facade |
Plus sdk/node/ for the Node.js binding.
If you prefer to build the renderer yourself:
git clone https://github.com/sinameraji/camouflage.git
cd camouflage
cargo install --path crates/tuiOr pipe NDJSON directly:
cargo run --release -p fake-agent -- --tokens 50000 --tools 200 | \
cargo run --release -p camouflage-tui -- --stdin-eventsApache-2.0