You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -8,30 +8,119 @@ Research findings for the oxide-code TUI, based on analysis of reference project
8
8
9
9
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.
10
10
11
-
**Key patterns**:
11
+
#### Key Patterns
12
12
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 system — CSS-like theming via `ThemedBox` / `ThemedText` with terminal color adaptation.
19
19
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.
21
65
22
66
### opencode (TypeScript / @opentui + Solid.js)
23
67
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.
- 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
25
119
26
-
**Key patterns**:
120
+
`routes/session/footer.tsx`
27
121
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.
|`ratatui-textarea`| Multi-line text input widget with cursor, selection, undo / redo |
77
166
78
167
### Visual Polish
79
168
@@ -113,11 +202,11 @@ This multiplexes all event sources into a single loop. Render is triggered after
113
202
114
203
Tokens arrive mid-syntax (e.g., `**part` then `ial**`). Approaches:
115
204
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.
118
207
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.
119
208
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.
121
210
122
211
## Reference Apps
123
212
@@ -140,7 +229,7 @@ Based on the research, the following decisions guide the TUI implementation:
140
229
2.**Component trait pattern** for UI architecture. Each view (chat, input, status, tool display) is a self-contained component.
141
230
3.**Synchronized output** enabled by default. Wrap every frame in DEC 2026 sequences.
142
231
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