feat(tui): Phase 2B — OpenTUI renderer + --tui CLI flag - #75
Merged
Conversation
Ships the minimum-viable interactive TUI promised by Phase 1's design
doc §8. Single screen, framed task list + log pane + progress bar +
keymap hint. Built on top of Phase 2A's pure-function foundation —
zero changes to the reducer/state/selectors.
What's wired:
- `src/tui/tui-shim.ts` — single import site for @opentui/react
(createCliRenderer, createRoot, useKeyboard, useTerminalDimensions).
Replacing the renderer = swap this file.
- `src/tui/components/{Header,TaskList,LogPane,ProgressBar,StatusBar}.tsx`
— props-only React components. Header shows run id, status counts,
parallel %, remote-cache indicator. TaskList renders one row per
task with status icon + duration; selected row highlighted.
LogPane shows the selected/pinned task's buffered output.
ProgressBar is N/M complete + filled glyph + parallel %.
- `src/tui/App.tsx` — composes the components; keyboard handler maps
j/k arrows to selectTask, ?/esc to overlay toggles, q/Ctrl-C to
exit. Single view for Phase 2B (state.activeView=1).
- `src/tui/tui.ts` — entry. Creates the OpenTUI renderer in
alt-screen mode, wraps the Phase 2A reducer + Observer adapter,
runs a 33ms paint debouncer + 1 Hz sparkline sampler tick.
`startTui({ testing: true })` exposes the headless renderer for
the smoke test.
- `--tui` / `--no-tui` parsed in src/cli/run.ts. shouldUseTui()
decides whether to mount the renderer; falls back silently to the
framed-block path when disqualified. Explicit --tui prints
`vx: TUI unavailable (<reason>)` when blocked. Lazy-loads
`src/tui/tui.ts` only when use=true so non-TUI runs pay zero
cold-start cost from @opentui/*.
Tests: 495 → 497.
- tests/cli.test.ts: --tui / --no-tui parser coverage.
- tests/tui-smoke.test.ts: mounts OpenTUI in headless mode,
dispatches runStart+runEnd, disposes idempotently. Catches
import-time and lifecycle regressions.
Tooling:
- tsconfig.json gains `jsx: react-jsx` + `jsxImportSource:
@opentui/react`.
- react@19 + @types/react@19 added as deps (peer of @opentui/react).
Compile-gate experiment (docs/design/tui-design.md §2): OpenTUI
mounts cleanly under Bun directly (no compile step), so the native
lib is loaded via @opentui/core's own initializer. The
`bun build --compile` single-binary path is deferred — when we ship
a compiled vx, we revisit and follow path (1) sibling-file install
from the design doc.
Manual e2e:
- `vx run build --tui` in a fixture project falls back silently
when stdin/stdout isn't a TTY (correct).
- `CI=1 vx run build --tui` prints
"vx: TUI unavailable (stdout is not a TTY)" and runs framed
output.
https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
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
Ships the minimum-viable interactive TUI promised by Phase 1's design
doc §8. Built on top of Phase 2A's pure-function foundation — zero
reducer/state/selector changes. Single screen for now (multi-view +
overlays land in Phase 3).
Tests: 495 → 497. Manual
--tuie2e verified.Components
src/tui/tui-shim.ts@opentui/react— replacing the renderer = swap this one file.src/tui/components/Header.tsxsrc/tui/components/TaskList.tsxsrc/tui/components/LogPane.tsxsrc/tui/components/ProgressBar.tsx▇▇▇░░ N/M X% parallelstrip.src/tui/components/StatusBar.tsxsrc/tui/App.tsxsrc/tui/tui.tsCLI
--tui/--no-tuiparsed insrc/cli/run.ts.shouldUseTui()decides whether to mount. Falls back silentlyto the framed-block path when disqualified.
--tuiprintsvx: TUI unavailable (<reason>)whenblocked. Reason strings exactly match the test matrix from Phase 2A.
src/tui/tui.ts. Non-TUI runs pay zerocold-start cost from
@opentui/*(~30–60 ms saved per invocation).Compile-gate (design doc §2)
OpenTUI mounts cleanly under Bun directly — no compile step needed
for the dev/run path. The single-binary
bun build --compilestoryis deferred until we actually ship a compiled
vx; we'll then followpath (1) sibling-file install from the design doc.
Tooling
tsconfig.jsongainsjsx: react-jsx+jsxImportSource: @opentui/react.react@19+@types/react@19added as deps (peer of@opentui/react).React.ReactNodereturns (OpenTUI's intrinsicsreturn
ReactNode, notReactElement).Manual e2e
Test plan
bun src/bin.ts run format— cleanbun src/bin.ts run lint— 0 warnings, 0 errorsbun src/bin.ts run test— 497 pass, 0 failtests/tui-smoke.test.ts(mount + dispatch + dispose),tests/cli.test.ts--tui parser cases--tui,--no-tui, andCI=1 --tuidisqualifier path
What's NOT in this PR (deferred to Phase 3)
1-5keys for Graph / Workers / Bottlenecks / Queue)./).process-level SIGINT trigger).
https://claude.ai/code/session_016HXj6HW6bxSn8EYuKcxTD9
Generated by Claude Code