Skip to content

feat(tui): onboarding is a screen inside the app, not a program before it - #222

Open
plombeer31 wants to merge 1 commit into
valeryb/onboarding-config-statefrom
valeryb/onboarding-shell
Open

feat(tui): onboarding is a screen inside the app, not a program before it#222
plombeer31 wants to merge 1 commit into
valeryb/onboarding-config-statefrom
valeryb/onboarding-shell

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

Stacked on #220 (base is valeryb/onboarding-config-state, retarget to main once 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:

  • two raw stderr lines sit above the setup screen — a config path and 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;
  • the screen cannot react to a resize, and has no size handling at all (fixed padding={1}, ~55-column option labels);
  • nothing it starts can outlive it. LocalModelsOrchestrator is constructed later, inside ChatOrchestrator, 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/0err and all.

What changes

The flow lives inside TuiApp. state.onboarding is a slice rather than a third TuiUiMode: twenty-one modules branch on uiMode and 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 (flexGrow spacer + flexShrink={0} footer inside the root box that is already sized to the terminal).

Keys. handleAppKey swallows everything while the flow is up, so a keystroke is never acted on twice by both the app and the screen's own useInput. 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. routeProvidersWizardKey is extracted from handleProvidersTabKey and 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's succeeded / closed also 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. setConfigNoticeSink lets 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.

onOnboardingFinished reloads 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 to local-backend-readiness.ts along with their 16 tests; the file is renamed rather than rewritten, so the diff shows it.

Sizing advises, never blocks. onboarding-fit.ts is a React-free table (same reasoning as splash-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:

  • first frame is the flow itself — no stderr above it, brand lockup at the top, hints on the last row;
  • j / digits / Enter move and pick; 2 opens 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 reports fetch failed in place, Esc steps back;
  • Esc at the choice lands in the agent, writes 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, no R U N / SESSIONS / composer text, hint strip is the last row, Esc records a skip). Ported: 16 predicate tests in local-backend-readiness.test.ts.

Full suite: 5150 passed. The two failures on this machine (fs-glob-real, send-message-concurrency) fail identically on main.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant