feat(tui): onboarding is a screen inside the app, not a program before it - #222
Open
plombeer31 wants to merge 1 commit into
Open
feat(tui): onboarding is a screen inside the app, not a program before it#222plombeer31 wants to merge 1 commit into
plombeer31 wants to merge 1 commit into
Conversation
…e it The first-run flow ran as its own Ink render before `enterAltScreen()` and before the runtime existed. Three consequences, all visible on a fresh install: two raw stderr lines sit above the setup screen (a config path and `local-llm unreachable …`), the screen cannot react to a resize, and nothing it starts can outlive it — the local branch had no orchestrator to own a download, so it handed the operator to the MANAGE ▸ LLM admin panel instead. Moves the flow inside `TuiApp` as a full-screen route: - `state.onboarding` — a slice, not a third `TuiUiMode`. Twenty-one modules branch on `uiMode`; a new variant would have to be considered in every one of them, and nothing branches on a slice it does not read. Non-null means the flow owns the terminal: no status bar, no rail, no composer, no hint strip but its own, pinned to the real last row by the existing flex-pressure idiom. - `handleAppKey` swallows keys while it is up, so a keystroke is never acted on twice — except Ctrl+C, which must quit from setup as it quits from anywhere else. - The cloud step *is* the providers wizard: `routeProvidersWizardKey` is extracted from `handleProvidersTabKey` and shared, so both surfaces drive one wizard through one code path, verification and hot-swap included. Its `succeeded` / `closed` outcomes move the flow, which is why the onboarding reducer sits ahead of the providers slice and delegates the panel half rather than duplicating it. - Config diagnostics get a sink (`setConfigNoticeSink`). The TUI collects them and replays them into the transcript; every other caller keeps stderr. That is the whole fix for the lines above the interface. - `onOnboardingFinished` reloads the runtime's providers. The old gate never needed this because no runtime existed when it ran. - The startup gate and its wizard component are deleted. Its three predicates outlived it and move to `local-backend-readiness.ts` with their tests. Sizing advises, never blocks: below 100×30 the surface sheds its explainer and row details and adds one footer line, and every key still works at every size. The local branch still hands over to the LLM tab for now — the standalone model picker is the next PR in the series. What changes here is where the flow lives, not yet what each screen says.
This was referenced Aug 21, 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.
Stacked on #220 (base is
valeryb/onboarding-config-state, retarget tomainonce that merges).Why
The first-run flow renders before
enterAltScreen()and before the runtime exists (tui-command.ts: gate →enterAltScreen()→render(TuiApp)). Three things follow, all of them visible on a fresh install:local-llm unreachable at http://127.0.0.1:8080 — starting setup…— so the first words a new user reads are a path and an error;padding={1}, ~55-column option labels);LocalModelsOrchestratoris constructed later, insideChatOrchestrator, so the local branch had no owner for a download and handed the operator to the MANAGE ▸ LLM admin panel instead — tab strip,kv —,tools 0ok/0errand all.What changes
The flow lives inside
TuiApp.state.onboardingis a slice rather than a thirdTuiUiMode: twenty-one modules branch onuiModeand a new variant would have to be considered in each; nothing branches on a slice it does not read. Non-null means the flow owns the terminal — no status bar, no rail, no composer, no hint strip but its own, pinned to the true last row by the existing flex-pressure idiom (flexGrowspacer +flexShrink={0}footer inside the root box that is already sized to the terminal).Keys.
handleAppKeyswallows everything while the flow is up, so a keystroke is never acted on twice by both the app and the screen's ownuseInput. Ctrl+C is the deliberate exception — quitting from setup must work exactly as it does everywhere else.The cloud step is the providers wizard, not a copy of it.
routeProvidersWizardKeyis extracted fromhandleProvidersTabKeyand shared by both surfaces, so Esc cannot come to mean different things in the two places the wizard appears, and the flow inherits key verification + save + hot-swap for free. The wizard'ssucceeded/closedalso move the flow, so the onboarding reducer sits ahead of the providers slice in the chain and delegates the panel half (a handled action never reaches the rest of the chain — without the delegation the panel would keep a stale wizard).Config diagnostics get a sink.
setConfigNoticeSinklets the TUI collect "created default config …" / "migrated config …" and replay them into the transcript; every other caller keeps stderr, unchanged. That is the entire fix for the lines above the interface.onOnboardingFinishedreloads the runtime's providers after the flow writes config — the old gate never needed this, because no runtime existed when it ran.Deletions. The startup gate (
run-local-models-config-wizard.ts) and its wizard component are gone. The three predicates that outlived it (isCloudTextProviderReady,isLocalBackendConfigured,isManagedModeReadyOnDisk) move tolocal-backend-readiness.tsalong with their 16 tests; the file is renamed rather than rewritten, so the diff shows it.Sizing advises, never blocks.
onboarding-fit.tsis a React-free table (same reasoning assplash-fit.ts): below 100×30 the surface drops its explainer and row details and adds one footer line; on a genuinely tiny terminal it sheds the mark too. Every key works at every size.Scope
The local branch still hands over to the LLM tab — the standalone model picker is the next PR. Where the flow lives changes here; what each screen says is PRs 4–5.
Verified in a real terminal
PTY + pyte capture at 100×30 against an empty state dir:
j/ digits / Enter move and pick;2opens the provider list and arrow keys reach it; Esc from the wizard returns to the choice rather than dropping out;3→ URL step with the line editor, Enter probes and reportsfetch failedin place, Esc steps back;skippedAt, and the second launch goes straight to the agent — the picker no longer returns on every start.Tests
New:
onboarding-key-bindings.test.ts(key table, including "Ctrl+C is not claimed" and "child-owned steps swallow but do not act"),onboarding-reducer.test.ts(cursor wrap, stale-error clearing, closed-flow no-ops, and the three wizard-delegation cases),onboarding-fit.test.ts(size table),onboarding-screen.test.tsx(frame assertions: the three choices, noR U N/SESSIONS/ composer text, hint strip is the last row, Esc records a skip). Ported: 16 predicate tests inlocal-backend-readiness.test.ts.Full suite: 5150 passed. The two failures on this machine (
fs-glob-real,send-message-concurrency) fail identically onmain.