11 KiB
TUI Research & Build Plan for Claw Code
Status: Draft | Date: 2026-06-12 | Branch: feat/tui
Executive Summary
This document synthesizes research on how similar tools build terminal UIs and proposes a phased plan for adding a first-class TUI to claw. The existing CLI already embeds crossterm, rustyline, pulldown-cmark, and syntect — giving us a strong foundation. The TUI should be a new mode (claw tui or claw --tui) rather than a replacement, sharing the ConversationRuntime, Session, and tool surfaces already in crates/runtime.
Prior Art: How Peers Build TUIs
1. OpenAI Codex CLI (codex-rs/tui)
The closest direct prior art — a Rust TUI for an AI coding assistant.
| Component | Crate / Pattern |
|---|---|
| Framework | ratatui + crossterm |
| Event loop | tokio-async with EventBroker and frame-rate limiter |
| Layout | App (top-level state machine) → ChatWidget (history cells + in-flight streaming cell) → bottom_pane (composer/input) |
| Markdown | pulldown-cmark |
| Syntax highlight | syntect |
| Clipboard | arboard |
| Testing | insta snapshot testing |
Key architectural decision: top-level state machine (App) that owns widget dispatch. Chat history is a scrollable list of "cells" (completed turns + one in-progress streaming cell). Input is a separate widget. This cleanly separates streaming concerns from input handling.
2. aichat (sigoden)
A popular all-in-one LLM CLI.
| Component | Crate / Pattern |
|---|---|
| REPL | reedline (nushell line editor) + crossterm |
| Prompts | inquire for interactive dialogs |
| Streaming | render/ module with MarkdownRender, markdown_stream, raw_stream |
| Syntax highlight | syntect |
| Color | ansi_colours |
Key insight: aichat shows that reedline is a stronger modern alternative to rustyline for rich REPLs, but the existing rustyline integration in claw can stay untouched if the TUI is a separate surface. The TUI should be a new surface, not a REPL replacement.
3. crush (liljencrantz)
An advanced Rust shell.
| Component | Crate / Pattern |
|---|---|
| REPL | rustyline (with file history) |
| Terminal control | termion |
| Layout | unicode-width for proper text measurement |
Key insight for claw: crush demonstrates shell/parser separation — the REPL is thin, and the heavy lifting (parsing, execution) lives in shared modules. This mirrors how claw should build its TUI: thin TUI shell, shared runtime core.
4. Other Rust TUI Patterns (from ecosystem research)
- Pimalaya (
himalayamail client): TUI as an add-on to existing CLI using feature flags; core logic is model-driven, UI is view-only. - Vuls (security scanner): Terminal viewer uses vim-like keybindings; scan results shared between JSON/CLI/TUI outputs.
- halp: Rust CLI tool with optional TUI mode via feature flags or runtime detection.
- TUIfying guides (Jack Bisceglia, Isakdl): Show layering a TUI atop CLI logic without regressing headless usage — confirming the dual-surface strategy.
Crate Ecosystem: Recommended Stack
| Layer | Crate | Rationale |
|---|---|---|
| TUI Framework | ratatui |
De-facto standard; layout engine, widget system, backend abstraction over crossterm/termion. Already the choice of codex-rs. |
| Backend | crossterm (already in use) |
Cross-platform; claw already depends on it. |
| Async bridge | tokio (already in use) |
ratatui + crossterm + tokio via crossterm::event::EventStream for async event handling. |
| Input widget | tui-textarea or tui-input |
Pre-built text input widget with scrolling, wrapping, key handling. tui-textarea is mature and widely used. |
| Markdown render | pulldown-cmark → custom widget (already in use) |
claw already has render.rs using pulldown-cmark + syntect. Port/adapt to a ratatui::widgets::Widget. |
| Syntax highlight | syntect (already in use) |
No change needed — wire existing HighlightLines into a ratatui Paragraph widget. |
| Clipboard | arboard (optional) |
For "copy code block" or "copy response" — nice-to-have for MVP. |
Crate not recommended:
tui-rs— deprecated, succeeded byratatui.termion—crosstermis already in the dependency tree; doubling backends adds pain.reedline— great for REPL, but the TUI is a full-screen app;ratatuiowns input.
Architecture: Proposed Design
Guiding Principles
- Non-breaking:
claw prompt,claw --help, and the REPL stay exactly as they are. - Shared core: The TUI consumes
ConversationRuntime,Session,ToolExecutor— no duplication. - Alt-screen only: TUI runs in the alternate screen buffer; exiting returns to the shell cleanly.
- Feature-flag friendly: Start as always-on dependency, gate behind
tuifeature if binary bloat becomes an issue.
Module Layout
rust/crates/rusty-claude-cli/src/
main.rs # existing — add `claw tui` dispatch
cli.rs # existing — add `Tui` to CliAction
tui/
mod.rs # public entry: run(env) -> Result
app.rs # App: top-level state machine (Screen enum)
event.rs # EventBroker: bridges crossterm events → App events
widgets/
chat.rs # ChatWidget: scrollable message list
input.rs # ComposerWidget: multi-line input + send
sidebar.rs # Optional: session list / tool status
render.rs # Markdown-to-ratatui Paragraph rendering (adapts existing render.rs)
State Machine: App
enum Screen {
Chat { // primary chat view
messages: Vec<MessageCell>,
scroll: ScrollState,
input: InputState,
streaming: Option<StreamingCell>,
},
SessionPicker, // list saved sessions (nice-to-have)
Help, // keybindings overlay
Quit, // shutdown signal
}
Event Flow
┌─────────────┐ ┌─────────────────┐ ┌──────────┐
│ Crossterm │────▶│ EventBroker │────▶│ App │
│ (keyboard) │ │ (tokio stream) │ │ (update) │
└─────────────┘ └─────────────────┘ └────┬─────┘
│
┌────────────────┘
▼
┌───────────────┐
│ ratatui::Frame│────▶ terminal draw
└───────────────┘
Integration with Existing Runtime
The TUI reuses these existing types directly (no wrappers):
| Existing Type | TUI Usage |
|---|---|
ConversationRuntime |
Drive turns; receive AssistantEvent / TurnProgressReporter |
Session / SessionStore |
Persist and resume chat history |
ToolExecutor / execute_tool |
Execute tool calls from streaming responses |
render::TerminalRenderer |
Adapt to ratatui Paragraph / Text |
input::LineEditor |
Not used; TUI has its own tui-textarea input |
The runtime emits events via TurnProgressReporter — the TUI provides its own reporter that writes into a tokio::sync::mpsc channel consumed by the event loop.
Implementation Roadmap
Phase 0: Prep (this PR)
- Create
feat/tuibranch ✓ - Add
ratatui,tui-textarea(and optionallycrosstermwithevent-streamfeature) torusty-claude-cli/Cargo.toml - Ensure
cargo check --workspacepasses
Phase 1: Skeleton (MVP)
tui/mod.rs—run_tui()entrypoint: init terminal (alternate screen, raw mode), run event loop, restore on exit.tui/event.rs—EventBroker:crossterm::event::EventStream→AppEvent(Tick,Key,Resize,Backend(AssistantEvent)).tui/app.rs— MinimalAppwith oneScreen::Chat. State: message list (mock data) + input field.tui/widgets/chat.rs—ChatWidget: rendersVec<MessageCell>asListor verticalLayoutofParagraphs.tui/widgets/input.rs—ComposerWidget: wrapstui_textarea::TextArea, handles Enter-to-send, Shift+Enter for newline.- Navigation —
Ctrl+Corqto quit;Ctrl+Lto clear; arrow keys to scroll. - CLI hook — Add
claw tuicommand that callsrun_tui().
Phase 2: Live Conversation
- Wire
ConversationRuntime::run_turn()into the TUI event loop. - Create a
TuiProgressReporterimplementingTurnProgressReporterthat sendsAppEvent::AssistantDeltainto the channel. - Render streaming assistant responses in real-time (append to
StreamingCell, convert toMessageCellon finish). - Render tool calls (spinner + result) inline in the chat.
- Session persistence: auto-save on exit,
/resumeintegration.
Phase 3: Polish
- Markdown widget — Port
render.rs(pulldown-cmark+syntect) to a ratatui widget with proper soft-wrap, code blocks, and inline formatting. - Sidebar — Show session list or active tool status in a third pane.
- Search / filter — Slash command picker, message search.
- Copy / clipboard —
arboardintegration for copying code blocks. - Theme — Respect
NO_COLOR, dark/light palette, user config.
Phase 4: Hardening
- Graceful degradation when terminal is too small.
- Mouse support (optional, via
crossterm). cargo test— unit tests for widgets, snapshot tests for render output.- Clippy clean, format check, docs.
Open Questions
- Should the TUI replace the REPL? No — keep both. The REPL is lightweight for quick queries; the TUI is immersive for long sessions.
- Should we add a
tuifeature flag? Start without one (simpler). Add if binary size becomes a concern. - How to handle permission prompts in the TUI? Modal overlay (
ratatui-popupor custom overlay widget) rather than spawning a subprocess. - Should the TUI support multiple conversation tabs? Nice-to-have; defer to Phase 3+.
Sources
- OpenAI Codex CLI (codex-rs/tui) — primary prior art
- sigoden/aichat — REPL + streaming patterns
- liljencrantz/crush — shell/parser separation
- Ratatui — TUI framework
- Pimalaya blog: Designing a TUI — architectural separation
- Jack Bisceglia: TUIfying Rust CLIs — layering TUI on CLI
- Isakdl: How I TUI-fy my Rust CLIs — practical guide
- halp — optional TUI mode
- Vuls — terminal viewer patterns