Skip to content

feat(tui): full-screen install wizard (Redesign v5)#134

Merged
fullstackjam merged 12 commits into
mainfrom
feat/tui-redesign-v5
Jul 9, 2026
Merged

feat(tui): full-screen install wizard (Redesign v5)#134
fullstackjam merged 12 commits into
mainfrom
feat/tui-redesign-v5

Conversation

@fullstackjam

Copy link
Copy Markdown
Member

What does this PR do?

Replaces the interactive openboot install experience with a single full-screen bubbletea wizard (boot probe → two-pane select → git identity capture → live streaming install), implementing the "OpenBoot TUI - Redesign v5" design.

Why?

The current interactive flow is a linear console script (huh prompts + scrolling output). The v5 redesign turns it into one coherent keyboard-driven TUI: real environment probes up front, loadout presets, a filterable two-pane catalog with already-installed detection, and a live install pipeline (per-phase sidebar + $cmd/✓result log + progress bar) driven by real events streamed from the install engine.

Key architecture: a new internal/progress sink lets brew/npm emit structured events instead of drawing their own progress bars when the TUI owns the terminal. With no sink registered, console behavior is unchanged--silent, --dry-run, -p, --from, -u, and the sync path keep their exact existing flows; only bare interactive openboot install on a TTY reaches the wizard.

Testing

  • go vet ./... passes (plus archtest; baseline updated for two line-number shifts only)
  • Relevant tests added or updated
  • Tested locally (./openboot install --dry-run or similar)

Verification layers, strongest first:

  1. Differential binary test: built main and this branch, ran both on 5 non-TUI paths (preset/silent/dry-run/packages-only/skip-flags/version) — output byte-identical after home-path normalization.
  2. L1 full suite green (unit + integration + contract + archtest), incl. an e2e test driving the real goroutine → sinks → ApplyContext → channel → Update loop against a dry-run plan.
  3. L3 pty tests (run locally, install nothing): launch/render/quit smoke + full keyboard choreography up to the final confirm, on a real pseudo-terminal via script(1).
  4. L4 real install through the TUI (TestVM_WizardTUI_RealInstall, runs on this PR's CI): replays the same key sequence through a real install on the macos-14 VM, then asserts brew/git system state.

The choreography test caught a real bug before CI: bubbletea coalesces pasted/fast text into one multi-rune key event, which the filter/git inputs dropped — fixed in f71da90.

Cross-repo checklist

  • Does this need a docs/content update in openboot.dev? — likely yes eventually (screenshots/GIFs of the new interactive flow), but nothing in this PR changes documented flags or commands.
  • Does this change the CLI ↔ server API contract? — No. No server API changes; catalog fetch/caching untouched.

Notes for reviewer

  • Deliberate behavior changes in the interactive flow (per the design's "every run also restores…" footnote): system config (oh-my-zsh, dotfiles, macOS prefs) now applies as defaults-on instead of per-step prompts; a minimal git name/email screen appears only when no identity is configured (fresh Macs); the screen-recording reminder is suppressed during the TUI (plan.Silent = true guards all mid-alt-screen prompts).
  • Riskiest part is the brew/npm loop change (stepStart/stepDone split). It's hard-gated on if streaming(); the differential test above is the direct evidence the non-streaming path is unchanged.
  • testutil.RunInteractive now paces expect sends 1 char / 20ms (send -s) — hardens all interactive VM tests against key coalescing, not just the new one.
  • Pre-existing gofmt drift in testutil/helpers.go, test/e2e/macos_defaults_e2e_test.go, test/e2e/vm_helpers_test.go (present on main) is intentionally left untouched.

https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo

Add internal/progress, a leaf package defining the streaming event
contract (Event + Sink) between the install engine and a live renderer.

brew and npm gain SetProgressSink: when a sink is registered they emit
structured StepStart/StepOK/StepFail events (with the command and phase)
instead of drawing their own ui.StickyProgress bar, letting a caller own
the terminal. With no sink the console behavior is byte-for-byte
unchanged. The two exec.Command baseline entries shifted by the added
import lines; baseline regenerated accordingly.

This is the plumbing the install TUI needs to render a live pipeline.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
PlanFromSelection builds a ready-to-Apply InstallPlan from an explicit
package selection without any interactive prompts — it categorizes the
selection and fills the system-config defaults (existing git identity,
oh-my-zsh, dotfiles, macOS prefs), honoring --packages-only and the
--shell/--macos/--dotfiles skip flags. Git identity is reused from the
existing global config when present and skipped otherwise, since the TUI
has no name/email screen.

This is the non-prompting counterpart to planInteractive, used by the
install wizard.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
Add internal/ui/tui/wizard: a single full-screen bubbletea program that
flows boot-probe → two-pane select → live pipeline install, under a
persistent title bar and status bar — the OpenBoot TUI Redesign v5.

- boot: animated real probes (hardware, Homebrew health, installed-tool
  scan, catalog) then a loadout picker backed by the built-in presets.
- select: two-pane catalog with per-category counts, substring filter,
  keyboard + mouse-free selection; already-installed packages are dimmed
  and non-toggleable.
- install: pipeline sidebar with per-phase progress, a live $cmd/✓result
  log, and a progress bar — driven by real events streamed from the
  install engine (brew/npm sinks + a Reporter bridged onto a channel).
  The Apply run is forced non-interactive so no prompt fires mid-screen;
  stray engine stdout is redirected away from the alt-screen.

Bare `openboot install` on a TTY now launches the wizard. Explicit
sources (-p, --from, -u, sync), --silent, and --dry-run keep their
existing flows untouched.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
Adds a test that drives the real spawnInstall goroutine → brew/npm sinks
→ installer.ApplyContext → channel Reporter → Update loop with a dry-run
plan, asserting the model reaches done with every phase finished. This
covers the integration seam the other model tests stub out with synthetic
events.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
buildPhases previously took package-phase totals from the full selection,
but the engine emits progress events only for packages it actually
installs (already-present ones are filtered first). On a non-fresh Mac
that made the sidebar counts, progress bar, and "N packages" summary
overcount. buildPhases now excludes anything in the scanned installed set,
and the DONE summary derives its count from the package-phase totals.

Also refresh CLAUDE.md: the install flow now routes bare interactive runs
through internal/ui/tui/wizard, with a Where-to-Look row for it.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
Fresh Macs — the wizard's core audience — often have no git identity, and
the previous behavior silently skipped git setup. Add a minimal name/email
screen shown between select and install, but only when git isn't already
configured (and not with --packages-only). Existing identities skip it and
are reused as before.

The captured values flow into the plan (overriding SkipGit); the lookup is
behind a test seam so the routing is covered deterministically.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
…elper

npmStepDone reordered Increment before PrintLine; the original console
path printed the line first. Final output is identical either way, but
match the original ordering exactly so the non-TUI path is unchanged.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
Drive the compiled binary under script(1) with a fresh HOME: wait for the
boot probes to finish, quit with q from the loadout screen, and assert the
wizard rendered, exited 0, and restored the terminal (alt-screen leave).
Nothing is installed — boot probes are read-only and the test never
confirms an install.

This closes the gap the wizard unit tests can't reach: alt-screen entry/
exit, the global stdout redirect in wizard.Run, and clean teardown only
manifest with a TTY attached.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
Bubbletea coalesces pasted or fast-arriving text into a single multi-rune
KeyMsg; the filter and git-identity inputs only accepted single-character
messages, so pasting a package name (or expect-driven input) was silently
dropped. Accept any KeyRunes message in the two text-input contexts.

Found by the pty choreography e2e test — the first "/stow\r" burst arrived
as one coalesced key event and never reached the query.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
… (L4)

Close the last coverage gap — "a real install driven through the TUI" —
following the lazygit integration-test pattern: drive the real binary on a
pty, wait for on-screen markers (never fixed sleeps), then assert real
system state.

- L3 (runs locally / at release, installs nothing): FullChoreography walks
  boot → hand-pick → filter → toggle → confirm → git identity with every
  keystroke of the real path, quitting right before the final confirm.
- L4 (macos-14 VM, every PR): TestVM_WizardTUI_RealInstall replays the
  same key sequence through a real install via MacHost.RunInteractive
  (expect), then asserts brew actually installed the formula, the captured
  git identity was written, and oh-my-zsh landed in the isolated HOME.
- RunInteractive now paces sends one char per 20ms (expect send -s):
  bubbletea coalesces byte bursts into one key event, which drops
  multi-character sends.
- Git config is isolated via GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM so the
  identity screen appears deterministically and assertions don't touch
  the host's real config.

HARNESS.md gains a row for the new behaviour sensors.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
@github-actions github-actions Bot added installer Package installation logic brew Homebrew related tests Tests only ui Terminal UI docs labels Jul 8, 2026
- exhaustive: cover rHeader/rInfo/rMuted explicitly in reporterLogLine
  (intentional drops — headers drive phase activation, narration would
  drown the log)
- gocyclo: extract shouldLaunchWizard so runInstallCmd stays under the
  complexity ceiling
- gosec G118: annotate the stored cancel func (called on ctrl+c and
  install completion; gosec can't see across the model)

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
…ounting

Ten confirmed findings from the multi-agent review of PR #134, fixed as a
batch:

Consent (user decision: pre-flight review screen, defaults on):
- New confirm screen between select/git and install: shows packages, git
  identity, and toggleable rows for oh-my-zsh / dotfiles / macOS prefs.
  Nothing mutates the system without having been on screen; toggles gate
  the plan in startInstall.

CLI routing:
- shouldLaunchWizard now excludes --update (runUpdate was unreachable
  interactively) and --pick falls through to its existing error instead
  of being silently dropped.

Abort semantics:
- First ctrl+c during install requests an abort: cancels the context and
  keeps the TUI until the engine reports done (goroutine joined, stdout
  redirect not restored under its feet); second ctrl+c force-quits.
  Aborted runs return ErrAborted → CLI exits non-zero with a message.
- On error/abort, unfinished phases stay visibly unfinished instead of
  being check-marked.

Input correctness:
- Backspace trims one rune, not one byte (git fields + filter query) —
  multi-byte names no longer corrupt into invalid UTF-8 in git config.

TTY contention:
- In streaming mode brew no longer wires /dev/tty into cask installs; a
  sudo prompt fails fast as a visible step error instead of hanging the
  raw-mode TUI indefinitely.

Parity:
- Screen-recording reminder runs after the wizard exits, back on a
  normal terminal (installer.ShowScreenRecordingReminderAfterTUI).

Progress accounting — new streaming invariant: every planned package
produces exactly one terminal event (installed / failed / already-
installed skip):
- brew and npm emit skip events for already-installed and alias-resolved
  packages; installer state-file skips emit via brew/npm.EmitSkipped.
- npm partial-batch recoveries emit their StepOK.
- brew retry passes emit events (log no longer shows ✗ for packages that
  succeeded on retry); phase counters clamp at total.
- buildPhases counts the full plan; DONE footer counts actual installs.

E2e choreography (L3 + L4) updated for the confirm screen.

Claude-Session: https://claude.ai/code/session_01HLoiSd2k9EJJ1HPjjDgUgo
@fullstackjam fullstackjam merged commit 0463ad7 into main Jul 9, 2026
13 of 14 checks passed
@fullstackjam fullstackjam deleted the feat/tui-redesign-v5 branch July 9, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

brew Homebrew related docs installer Package installation logic tests Tests only ui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant