feat(tui): full-screen install wizard (Redesign v5)#134
Merged
Conversation
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
- 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
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.
What does this PR do?
Replaces the interactive
openboot installexperience 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/✓resultlog + progress bar) driven by real events streamed from the install engine.Key architecture: a new
internal/progresssink 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 interactiveopenboot installon a TTY reaches the wizard.Testing
go vet ./...passes (plus archtest; baseline updated for two line-number shifts only)./openboot install --dry-runor similar)Verification layers, strongest first:
mainand 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.goroutine → sinks → ApplyContext → channel → Updateloop against a dry-run plan.script(1).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
openboot.dev? — likely yes eventually (screenshots/GIFs of the new interactive flow), but nothing in this PR changes documented flags or commands.Notes for reviewer
plan.Silent = trueguards all mid-alt-screen prompts).stepStart/stepDonesplit). It's hard-gated onif streaming(); the differential test above is the direct evidence the non-streaming path is unchanged.testutil.RunInteractivenow paces expect sends 1 char / 20ms (send -s) — hardens all interactive VM tests against key coalescing, not just the new one.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