feat(tui): rebuild on @opentui/solid + xterm-headless (opencode pattern) - #79
Merged
Conversation
Wholesale rewrite of the TUI. The prior @opentui/react implementation
was rejected as unusable ("the tui is trash :/ nothing works"). After
studying both Turbo's TUI (Rust, ratatui + vt100) and opencode's TUI
(the OpenTUI maintainers' own TUI), the path forward is clear:
opencode uses @opentui/solid, not @opentui/react. Solid's fine-grained
reactivity avoids the React-reconciler-vs-painter races that produced
our ghosting and overlay-bleed bugs.
What changed:
Deps:
- removed: @opentui/react, react, @types/react
- added: @opentui/solid, @opentui/keymap, solid-js,
xterm-headless, @types/babel__core
tsconfig:
- "jsx": "preserve", "jsxImportSource": "@opentui/solid"
bunfig.toml (new):
- preload = ["@opentui/solid/preload"] # for `bun run` / `bun test`
cli/run.ts:
- lazy `await import('@opentui/solid/preload')` before the dynamic
TUI import so installed-binary users (different cwd, no bunfig)
also get the babel-preset-solid Bun plugin registered
New src/tui/ layout (mirrors opencode):
context/helper.tsx createSimpleContext factory (port)
context/theme.tsx single dark theme + dialogBackdrop RGBA
context/run-state.tsx Solid createStore: byId / running /
planned / finished buckets, Turbo-style
finished-rank ordering (failures first)
context/pty-store.tsx per-task pty registry; lazy-creates an
xterm-headless Terminal on first chunk,
buffers chunks during async load
component/pty-output.ts xterm-headless wrapper with window/self
shims + allowProposedApi
component/task-list.tsx left sidebar; spinner for running,
✓/⊙/⨯/⊝ glyphs by terminal status
component/log-pane.tsx right pane; reads
buffer.active.getLine() for the
selected task — vt100-parsed output
renders correctly (progress bars,
cursor escapes, \r overwrites)
component/status-bar.tsx single padded line, q / ↑↓ / m hints
ui/dialog.tsx Dialog primitive — opencode's exact
pattern: position="absolute" full
viewport + zIndex={3000} + translucent
RGBA backdrop + popup centered inside
via alignItems="center"+paddingTop
overlay/help-dialog.tsx keymap legend dialog (m toggles, esc
closes), bound through DialogProvider
app.tsx App shell with useKeyboard, layout
TaskList + LogPane + StatusBar
tui.tsx tui() entry: createCliRenderer +
render(<ThemeProvider><RunStateProvider>
...<App /></DialogProvider>...)
should-use-tui.ts decision-table predicate (preserved)
Deleted ~3,300 lines of React-binding code:
src/tui/{components,overlays,primitives,state,views}/*
src/tui/{App.tsx, tui-shim.ts, tui.ts}
tests/{should-use-tui, sparkline, timeline-layout, critical-path,
tui-store, tui-selectors}.test.ts
Tests: 506 → 436 (deleted React-binding-specific tests + 5 internal
state/selector tests; kept all 414 orchestrator/cli/cache tests +
the Solid TUI smoke test). Smoke test mounts the new TUI under
OpenTUI's testing=true mode and dispatches the full event sequence
(runStart → taskStart → taskStdout → taskComplete → runEnd) — passes
cleanly.
See docs/design/tui-rebuild.md for the full Turbo + opencode
analysis and four-option comparison that led here.
https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
The TUI rebuild commit's decision-log entry hit an oxfmt issue — a stray list-marker indentation rewrites cleanly with `oxfmt .`. No content change.
This was referenced May 13, 2026
Exelord
pushed a commit
that referenced
this pull request
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
You said the TUI was trash and asked if we could mirror opencode's
approach. Done. Wholesale rewrite of the TUI on the stack opencode
actually uses, plus
xterm-headlessfor per-task log panes (whichopencode doesn't need but we do).
The full Turbo + opencode analysis lives in
docs/design/tui-rebuild.md.Stack swap
@opentui/react@opentui/soliduseKeyboard@opentui/keymapavailablezIndex={3000}\r/ANSI)xterm-headlessvt100 emulator per taskWhat's in
src/tui/The Dialog primitive is the key thing I was missing before:
This is exactly opencode's pattern. No more bleed-through cells.
Tooling
"jsx": "preserve","jsxImportSource": "@opentui/solid"bunfig.toml(new):preload = ["@opentui/solid/preload"]forbun runandbun testcli/run.tsdoes an explicitawait import('@opentui/solid/preload')before lazy-loading the TUI so installed-binary users (their own
cwd, no bunfig) also get the babel-preset-solid Bun plugin
registered
Deps
What was deleted
~3,300 LOC of React-binding TUI code:
src/tui/{components,overlays,primitives,state,views}/*src/tui/{App.tsx, tui-shim.ts, tui.ts}What survived: the orchestrator-side Observer + scheduler-slot
allocation + cache history (Phase 1) — that work was clean and
binding-agnostic.
Test plan
bun src/bin.ts run format— cleanbun src/bin.ts run lint— 0/0bun src/bin.ts run test— 436/436 passtests/tui-smoke.test.tsmounts the Solid TUI in OpenTUItesting: truemode and dispatches the full event sequence(runStart → taskStart → taskStdout → taskComplete → runEnd)
from inside the bash tool; needs your eyes when you're back
What's NOT in this PR (deliberately cut scope)
opencode both ship one screen.
@opentui/keymapdeclarative bindings. We use plainuseKeyboardfor now; can migrate once we want priorities/contexts.
/). Easy to add as a follow-up dialog.https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
Generated by Claude Code