Skip to content

Commit 27d74a5

Browse files
committed
docs(research): update TUI research with deep-dive reference analysis
Add detailed findings from source-level analysis of claude-code and opencode, organized under h4 headings per topic area: - claude-code: streaming markdown (stable-prefix / unstable-suffix, LRU token cache, fast-path regex skip), thinking display (collapsed by default), tool polymorphism, input (vim, history, autocomplete), virtual scrolling (height cache, viewport culling, cascade prevention). - opencode: tree-sitter WASM markdown, two-tier tool display (InlineTool vs BlockTool with truncation limits), thinking at 60% opacity, input (extmarks, prompt stash, $EDITOR integration), footer (LSP/MCP counts). Update streaming markdown strategy to document the stable-prefix cache approach. Update design decisions to reflect implemented choices. Fix ratatui-textarea crate name in ecosystem table.
1 parent e421d30 commit 27d74a5

1 file changed

Lines changed: 116 additions & 27 deletions

File tree

docs/research/tui.md

Lines changed: 116 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,119 @@ Research findings for the oxide-code TUI, based on analysis of reference project
88

99
claude-code uses a **custom fork of Ink** — a React-based terminal rendering engine with a custom reconciler. The rendering pipeline is: React component tree → Yoga Flexbox layout (pure TypeScript, no C++ bindings) → screen buffer diff → minimal ANSI output.
1010

11-
**Key patterns**:
11+
#### Key Patterns
1212

13-
- **Streaming-first components**: Every component is designed to handle partial / streaming data. Text tokens accumulate in React state; tool call JSON is parsed incrementally on each delta.
14-
- **Double-buffered frames**: The Ink instance maintains `frontFrame` and `backFrame` buffers, diffing them to emit only changed cells. This reduces terminal I/O but doesn't eliminate flicker because React's reconciler still triggers full tree traversals on every state change.
15-
- **ANSI parser as React component**: An `Ansi` component converts raw escape sequences from shell output into React-compatible `Text` spans, bridging imperative terminal output into the declarative component model.
16-
- **Collapsible tool groups**: `CollapsedReadSearchContent` groups repeated tool calls (e.g., multiple file reads) into a single expandable row, keeping the chat history scannable.
17-
- **Glimmer animation**: `GlimmerMessage` renders a shimmering progress indicator with elapsed time for long-running operations.
18-
- **Theme system**: CSS-like theming via `ThemedBox` / `ThemedText` components with terminal color adaptation.
13+
- Streaming-first components — every component handles partial / streaming data. Text tokens accumulate in React state; tool call JSON is parsed incrementally on each delta.
14+
- Double-buffered frames`frontFrame` / `backFrame` buffers, diffing to emit only changed cells. Reduces terminal I/O but doesn't eliminate flicker because React's reconciler still triggers full tree traversals on every state change.
15+
- ANSI parser as React component — an `Ansi` component converts raw escape sequences from shell output into React-compatible `Text` spans.
16+
- Collapsible tool groups`CollapsedReadSearchContent` groups repeated tool calls (e.g., multiple file reads) into a single expandable row.
17+
- Glimmer animation `GlimmerMessage` renders a shimmering progress indicator with elapsed time for long-running operations.
18+
- Theme systemCSS-like theming via `ThemedBox` / `ThemedText` with terminal color adaptation.
1919

20-
**Weakness**: Full-screen redraw on every React state change causes severe flickering in long sessions (anthropics/claude-code#1913 — 315 reactions). This is Ink's fundamental limitation.
20+
Full-screen redraw on every React state change causes severe flickering in long sessions (anthropics/claude-code#1913 — 315 reactions). This is Ink's fundamental limitation.
21+
22+
#### Streaming Markdown
23+
24+
`Markdown.tsx`, `utils/markdown.ts`
25+
26+
- Two-layer hybrid: `marked` lexer for tokenization, `chalk` for ANSI styling, `cli-highlight` (lazy-loaded via Suspense) for syntax highlighting in code blocks.
27+
- `StreamingMarkdown` splits content at the last *top-level block boundary* (not line). Maintains a monotonic `useRef` boundary — only the final growing block is re-parsed per delta, giving O(1) amortized cost regardless of total text length.
28+
- Module-level LRU token cache (500 entries, keyed by content hash) avoids re-parsing on virtual-scroll remount (~3 ms per `marked.lexer` call saved).
29+
- Fast-path regex check: scans first 500 chars for markdown syntax; if none found, skips the lexer entirely and returns a single paragraph token.
30+
- Tables are extracted and rendered as React components with flexbox layout; all other content is concatenated into ANSI strings and rendered via `<Ansi>`.
31+
32+
#### Thinking Display
33+
34+
`AssistantThinkingMessage.tsx`
35+
36+
- Collapsed by default: shows `"∴ Thinking"` in dim italic as a single line, with a `Ctrl+O` expand hint. Only expanded in verbose / transcript mode.
37+
- When expanded: `"∴ Thinking…"` header, then full thinking content rendered via `<Markdown dimColor>` with `paddingLeft={2}`.
38+
39+
#### Tool Display
40+
41+
`AssistantToolUseMessage.tsx`
42+
43+
- Highly polymorphic: each `Tool` object provides its own `renderToolUseMessage()`, `renderToolUseProgressMessage()`, and `renderToolUseQueuedMessage()`.
44+
- Status dot: dim `` (queued) → animated spinner (in progress) → error state.
45+
- Tool name rendered bold, optional background color and tags (timeout, model, resume ID).
46+
- Some tools are "transparent wrappers" — hide their name and show only progress.
47+
48+
#### Input
49+
50+
`PromptInput.tsx` (~2300 lines)
51+
52+
- Full vim emulation via `src/vim/` module (motions, operators, text objects, mode transitions).
53+
- Command autocomplete with typeahead suggestions, slash commands.
54+
- Arrow-key history navigation, history search.
55+
- Image paste detection from clipboard.
56+
57+
#### Virtual Scrolling
58+
59+
`VirtualMessageList.tsx`, `ScrollBox.tsx`
60+
61+
- `ScrollBox` bypasses React for scroll — `scrollTo` / `scrollBy` mutate DOM directly and schedule a throttled render. No React state per wheel event.
62+
- Height cache per message, invalidated on terminal width change. Viewport culling — only visible children rendered.
63+
- `React.memo` on `LogoHeader` prevents dirty-flag cascade through all `MessageRow` siblings (critical for long sessions — without it, 150K+ writes per frame).
64+
- `OffscreenFreeze` wraps static content to prevent re-renders. `useDeferredValue` for non-critical state updates.
2165

2266
### opencode (TypeScript / @opentui + Solid.js)
2367

24-
opencode uses **@opentui/core** with **Solid.js** for fine-grained reactive terminal rendering.
68+
opencode uses **@opentui/core** with **Solid.js** for fine-grained reactive terminal rendering. (Note: despite early documentation suggesting Go / Bubble Tea, the current implementation is a TypeScript monorepo.)
69+
70+
#### Key Patterns
71+
72+
- Fine-grained reactivity — Solid.js signals trigger surgical updates; only the specific text node receiving a new token re-renders, not the entire component tree. This avoids the redraw problem that plagues Ink.
73+
- SDK event-driven streaming — typed events (`message.part.updated`) applied to a Solid store via `produce()`. Dependent `createMemo` computations and UI nodes update automatically.
74+
- 30+ themes with auto-detection — JSON-defined themes with dark / light detection via ANSI OSC 11 query. Adaptive foreground contrast calculation. Theme priority: defaults < plugins < custom files < system.
75+
- Leader-key input — default `Ctrl+X` prefix for extended keybinds, reducing conflicts with terminal and shell bindings.
76+
- Plugin system — full API with command registration, custom routes, theme injection, and slot-based extension points.
77+
- Scroll acceleration — macOS-aware scroll speed with configurable acceleration curves.
78+
- Responsive layout — width breakpoint at 120 columns, sidebar toggling, `contentWidth = width - (sidebarVisible ? 42 : 0) - 4`.
79+
80+
#### Markdown Rendering
81+
82+
`routes/session/index.tsx`
83+
84+
- Uses tree-sitter WASM parsers for syntax highlighting (~20 languages declared in `parsers-config.ts`).
85+
- Two rendering modes: `<code filetype="markdown">` (standard) and `<markdown>` (experimental, behind a feature flag). Both accept `streaming={true}` for incremental parsing.
86+
- Concealment: toggle to hide markdown syntax characters (e.g., `**` for bold) — saves horizontal space.
87+
- Dedicated theme colors for each markdown element: `markdownHeading`, `markdownCode`, `markdownBlockQuote`, `markdownEmph`, etc.
88+
89+
#### Tool Display
90+
91+
`routes/session/index.tsx`
92+
93+
- Two-tier pattern:
94+
- `InlineTool`: compact one-liner with icon prefix (`` read, `` write, `$` bash, `` glob, `` grep, `` generic). Pending state shows `~ message` with spinner. Denied permissions render with strikethrough.
95+
- `BlockTool`: bordered panel (`` left border) with title, body content, and hover background. Used for tools with output.
96+
- Bash output capped at 10 lines with expand / collapse. Generic tools capped at 3 lines.
97+
- Entire tool detail layer is toggle-able via keybind — when hidden, completed tools vanish entirely.
98+
99+
#### Thinking Display
100+
101+
`routes/session/index.tsx`
102+
103+
- Left `` border in `backgroundElement` color (subtler than tool borders).
104+
- Content rendered at 60% opacity via `subtleSyntax()` — same syntax rules but with alpha-reduced foreground colors.
105+
- Prefixed with italic `_Thinking:_`. `[REDACTED]` tokens stripped.
106+
- Toggle-able via keybind or `/thinking` command.
107+
108+
#### Input
109+
110+
`component/prompt/index.tsx` (~1280 lines)
111+
112+
- Extmarks — virtual inline text markers for file references (`[Image 1]`), agent mentions, pasted text (`[Pasted ~N lines]`). Expanded inline on submit.
113+
- Prompt stash — push / pop prompt content for later use (switch context without losing draft).
114+
- `$EDITOR` integration — opens external editor with current prompt, reconciles extmark positions on return.
115+
- Shell mode entered by typing `!` at position 0.
116+
- `Meta+Enter` for newline (vs Shift+Enter).
117+
118+
#### Footer
25119

26-
**Key patterns**:
120+
`routes/session/footer.tsx`
27121

28-
- **Fine-grained reactivity**: Solid.js signals trigger surgical updates — only the specific text node receiving a new token re-renders, not the entire component tree. This avoids the redraw problem that plagues Ink.
29-
- **SDK event-driven streaming**: The SDK emits typed events (`message.part.updated`), which the Session component catches and applies to a Solid store via `produce()`. Dependent `createMemo` computations and UI nodes update automatically.
30-
- **30+ themes with auto-detection**: JSON-defined themes with dark / light detection via ANSI OSC 11 query. Adaptive foreground contrast calculation. Theme priority: defaults < plugins < custom files < system.
31-
- **Leader-key input**: Default `Ctrl+X` prefix for extended keybinds, reducing conflicts with terminal and shell bindings.
32-
- **Plugin system**: Full plugin API with command registration, custom routes, theme injection, and slot-based extension points (home footer, sidebar panels, session routes).
33-
- **Scroll acceleration**: macOS-aware scroll speed with configurable acceleration curves.
34-
- **Responsive layout**: Width breakpoint at 120 columns, sidebar toggling, `contentWidth = width - (sidebarVisible ? 42 : 0) - 4`.
122+
- Left: working directory. Right: LSP count (`• N LSP`), MCP count (`⊙ N MCP`) with error coloring, permission warnings, `/status` hint.
123+
- Subagent footer shows agent label, sibling index (e.g., "3 of 5"), token usage, parent / prev / next navigation.
35124

36125
## Flickering Prevention
37126

@@ -71,9 +160,9 @@ The terminal flickering problem (anthropics/claude-code#1913) affects most CLI-b
71160

72161
### Input & Interaction
73162

74-
| Crate | Purpose |
75-
| -------------- | ---------------------------------------------------------------- |
76-
| `tui-textarea` | Multi-line text input widget with cursor, selection, undo / redo |
163+
| Crate | Purpose |
164+
| ------------------ | ---------------------------------------------------------------- |
165+
| `ratatui-textarea` | Multi-line text input widget with cursor, selection, undo / redo |
77166

78167
### Visual Polish
79168

@@ -113,11 +202,11 @@ This multiplexes all event sources into a single loop. Render is triggered after
113202

114203
Tokens arrive mid-syntax (e.g., `**part` then `ial**`). Approaches:
115204

116-
1. **Line-based commit**: Buffer tokens, commit to the rendered view at `\n` boundaries. Prevents mid-tag visual glitches. Simple and effective.
117-
2. **Incremental parse**: `pulldown-cmark` supports event-based parsing. Process events as they arrive, re-parse the trailing incomplete line on each frame.
205+
1. **Line-based commit with stable-prefix cache**: Buffer tokens, commit to the rendered view at `\n` boundaries. Track a monotonic byte boundary — only lines beyond the cached boundary are re-parsed. The stable prefix is rendered once and stored as owned `Line<'static>` values. This gives O(new lines) per token instead of O(total text). Adopted by oxide-code; inspired by claude-code's block-level variant.
206+
2. **Block-level commit** (claude-code): Same idea but at pulldown-cmark block boundaries instead of line boundaries. Theoretically more precise (a code fence mid-line is still one block) but requires deeper parser integration.
118207
3. **Code block handling**: Buffer entire code blocks before applying syntax highlighting (avoids partial-highlight flicker), or apply a simple monospace style during streaming and re-highlight on block completion.
119208

120-
Recommendation: line-based commit for prose, buffered re-highlight for code blocks.
209+
Recommendation: line-based commit with stable-prefix cache for the initial implementation. Upgrade to block-level boundaries when viewport virtualization is added.
121210

122211
## Reference Apps
123212

@@ -140,7 +229,7 @@ Based on the research, the following decisions guide the TUI implementation:
140229
2. **Component trait pattern** for UI architecture. Each view (chat, input, status, tool display) is a self-contained component.
141230
3. **Synchronized output** enabled by default. Wrap every frame in DEC 2026 sequences.
142231
4. **Render throttling at ~60 FPS**. Batch streaming tokens between frames.
143-
5. **Line-based markdown commit** during streaming, full re-render on message completion.
144-
6. **Dark theme by default** with a curated palette (4–6 colors). Light theme as an option, detected via OSC 11 or config.
145-
7. **Collapsible tool groups** for repeated operations (inspired by claude-code's `CollapsedReadSearchContent`).
146-
8. **Viewport virtualization** for long conversations — only render visible messages.
232+
5. **Line-based markdown commit with stable-prefix cache** during streaming, full re-render on message completion. Monotonic boundary avoids O(n) re-parsing.
233+
6. **Catppuccin Mocha dark theme by default** with 11 named color slots. Transparent background to respect user's terminal theme.
234+
7. **Two-tier tool display** — inline summary with per-tool icons, plus truncated output body (inspired by opencode's InlineTool / BlockTool pattern). Truncation at 5 lines with overflow count.
235+
8. **Viewport virtualization** for long conversations — only render visible messages (planned).

0 commit comments

Comments
 (0)