Skip to content

feat(agent-ui): first-run onboarding wizard — hardware pre-flight, in-app model download, connect-on-install#2204

Merged
kovtcharov-amd merged 2 commits into
mainfrom
feat/first-run-onboarding-1726
Jul 18, 2026
Merged

feat(agent-ui): first-run onboarding wizard — hardware pre-flight, in-app model download, connect-on-install#2204
kovtcharov-amd merged 2 commits into
mainfrom
feat/first-run-onboarding-1726

Conversation

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Why this matters

Before: a first-time user had no guided path — models and connectors could only be set up from the terminal (gaia init), a machine that couldn't meet an agent's requirements only found out at runtime, and a failed model download was swallowed silently. After: opening the Agent UI on a fresh machine launches an in-app wizard that scans the hardware and gates on real capability (RAM/disk/NPU/GPU), downloads the recommended model with visible progress and retry, and offers to connect an app (e.g. Google for Email triage) before first run. A non-technical user reaches a working chat without touching a terminal.

Bundles the two first-run issues since they share one flow: #1726 (onboarding wizard + in-app model download) and #1727 (hardware pre-flight gating + connector-on-install).

Every failure state is loud and actionable — a hardware blocker disables Continue and names the shortfall, a failed download shows the backend's message with a Retry, and a missing OAuth credential shows exactly which env vars to set. Nothing silently skips to leave a half-set-up app.

What's in it

Test plan

  • Backend unit tests — python -m pytest tests/unit/test_onboarding_router.py tests/unit/test_hub_compatibility.py (36 passed): pre-flight tiers/blockers/warnings, NPU present/absent/unknown, GPU VRAM below/meets requirement, status/complete marker round-trip.
  • Frontend unit tests — npm run test in src/gaia/apps/webui (195 passed, incl. 13 new): useOnboarding state machine (advance/back/guard/skip/goTo) + wizard flow (welcome skip, scan, blocker gating, advance).
  • Type-check + build — npm run build (tsc + vite) clean.
  • Lint — black / isort / flake8 / pylint clean on changed files.
  • Manual first-run walkthrough (Playwright, real backend on a fresh ~/.gaia): welcome → hardware check (Tier: Full, RAM/disk/NPU all green, Continue enabled) → model download (Continue correctly gated until the model is ready; explicit "Set up later" escape) → Google connector (optional; loud "OAuth credentials not configured" with the exact env vars) → "You're all set". Screenshots captured for each step.

To verify locally:

# Terminal 1 — backend
python -m gaia.ui.server --port 4200 --host 127.0.0.1
# Terminal 2 — build + open (fresh machine: no ~/.gaia/chat/initialized)
cd src/gaia/apps/webui && npm ci && npm run build
# open http://127.0.0.1:4200 — the wizard overlays until setup completes

Part of #2014
Closes #1726
Closes #1727

… in-app model download, and connect-on-install

First-run users had no guided path: models and connectors could only be set up
from the CLI, and a failed hardware match or download surfaced late (or was
swallowed). This adds an in-app wizard that scans hardware, downloads the
recommended model with visible progress and retry, and offers to connect an app
before first run.

Backend:
- Feed detected NPU / GPU VRAM into hub.compatibility.check_compatibility so a
  no-NPU machine gets a real warning instead of a blanket "cannot verify"
  (#1727); values default to None to preserve the conservative fallback.
- New onboarding router: GET /api/onboarding/preflight (hardware scan + tier +
  recommended model, running the shared compatibility checker so blockers match
  the hub install path), plus /status and /complete over the existing
  ~/.gaia/chat/initialized marker the CLI and agent-loop already read.

Frontend:
- New components/onboarding/ wizard: welcome -> pre-flight -> model download ->
  optional connector -> done, driven by a pure, unit-tested state machine
  (useOnboarding). Model download reuses POST /api/system/download-model and its
  status-fed progress; the connector step reuses the existing authorize flow
  (no grant logic rebuilt). Every failure surfaces a loud, actionable state with
  retry -- no silent skip that leaves a half-set-up app.

Tests: Vitest for the state machine + wizard flow; pytest for the pre-flight
route and the detected-NPU/GPU compatibility wiring.
@github-actions github-actions Bot added tests Test changes electron Electron app changes labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Verdict: Approve with suggestions — safe to merge after a look at the one maintainability point below.

This adds a first-run onboarding wizard to the Agent UI: a hardware pre-flight that gates on real capability (RAM/disk/NPU/GPU), an in-app model download with progress + retry, and an optional connect-an-app step, all backed by three new /api/onboarding/* endpoints. It's cleanly isolated to new files, reuses the shared compatibility checker and existing download/connector flows rather than rebuilding them, fails loudly on every error path, and ships with solid tests on both sides.

The one thing worth a second look: the backend's Lemonade device probe (NPU/GPU/VRAM parsing) is a near-copy of logic that already lives in the system-status router. It's not a bug, but the two copies have already drifted slightly in how they treat a missing NPU, so they'll keep drifting unless one calls the other. Nothing here blocks merge.

No security concerns. No breaking changes — the init marker stays compatible with the existing .exists() readers.

🔍 Technical details

🟡 Important (non-blocking)

Duplicated Lemonade device probe — drift risk (src/gaia/ui/routers/onboarding.py:1688 vs src/gaia/ui/routers/system.py:607-625)
Both parse /system-info devices for GPU name / vram_gb / NPU availability. The two implementations already diverge: system.py never yields a definite "no NPU" (absence just omits npu from detected_devices), while the new probe distinguishes False (seen-but-unavailable) from None (unprobed). That richer three-state semantics is exactly what the pre-flight needs — so the cleaner fix is to lift it into one shared helper (e.g. probe_lemonade_devices() returning {lemonade_running, npu_detected, gpu_name, gpu_vram_gb}) and have the system router consume it too, so the NPU logic can't silently diverge again. Extract-and-share, not two copies. Not blocking, but a maintainer will thank you later.

🟢 Minor / nits

First download poll is delayed ~2s (ModelDownloadStep.tsx:326-327)
After downloadModel resolves, startDownload sets up setInterval(poll, POLL_MS) but doesn't fire poll() once immediately, so the progress bar sits empty for up to 2s after the click. One eager call tightens the feedback:

            stopPolling();
            poll();
            pollRef.current = setInterval(poll, POLL_MS);

New user-facing feature likely wants a doc line — the wizard and the /api/onboarding/* surface aren't mentioned in docs/guides/agent-ui.mdx. Per CLAUDE.md ("every new feature must be documented") a short note on the first-run flow would close the loop; the internal endpoints themselves don't need a reference page.

Strengths

  • Genuine reuse over reinvention: pre-flight runs the same check_compatibility the hub install path uses, download rides the existing POST /api/system/download-model + status SSE, and the connector step is explicit about not rebuilding grant logic. The detected_npu/detected_gpu_vram_gb wiring into the shared checker (with None preserving the conservative "cannot verify" path) is the right shape.
  • Fail-loudly throughout: the marker-write 500 with an actionable message (onboarding.py:1877), the download/preflight error banners with retry, and the "can't verify → keep the guard on" default all match the no-silent-fallback rule.
  • Well-tested both sides: the pure useOnboarding state machine, the wizard flow (blocker gating, advance), and the router (tiers, blockers, NPU present/absent/unknown, marker round-trip incl. the legacy empty-marker case) are all covered — and the marker stays .exists()-compatible with system.py:686 / agent_loop.py:216.

@kovtcharov-amd
kovtcharov-amd enabled auto-merge July 18, 2026 02:45
@kovtcharov

Copy link
Copy Markdown
Collaborator

The first-run golden path can dead-end frozen: ModelDownloadStep only creates its poll interval inside startDownload(); the mount effect calls poll() exactly once. If the user starts the multi-GB download and then remounts the step (page refresh — the wizard re-shows since the init marker isn't written yet — or Back/return), the single poll sees state === 'downloading', renders the progress bar, and never polls again. Progress stays frozen and Continue never enables even after the download completes; the only escape is skipping setup.

Fix: start the interval from the mount effect whenever the initial poll reports an active download (and clear it on unmount, as startDownload already arranges). Note this polling lifecycle is exactly the untested slice — the 36 backend + 13 frontend tests cover the router and wizard navigation, but not remount-mid-download; worth a test with the fix.

… freeze mid-download

Starting the multi-GB model download and then remounting the onboarding
wizard — a page refresh (the wizard re-shows until the init marker is
written) or Back/return navigation — left the progress bar frozen: the
mount effect polled exactly once, saw an in-flight download, rendered the
bar, and never polled again. Continue never enabled and the first-run
golden path dead-ended.

The mount effect now resumes the same polling loop when the initial status
reports an active download (downloading/starting). Both entry points share
a guarded beginPolling() helper so a remount can't stack two intervals, and
the loop still stops on unmount and on terminal states.

@kovtcharov kovtcharov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frozen-download flaw is fixed: polling is now extracted into beginPolling(), and the mount effect restarts it whenever the initial status reports an active download (downloading/starting) — so a remount mid-download resumes instead of freezing. Backed by a new 127-line ModelDownloadStep.test.tsx covering the remount-resume path. Verified on the PR branch.

@kovtcharov-amd
kovtcharov-amd added this pull request to the merge queue Jul 18, 2026
Merged via the queue into main with commit d7cffc7 Jul 18, 2026
43 of 46 checks passed
@kovtcharov-amd
kovtcharov-amd deleted the feat/first-run-onboarding-1726 branch July 18, 2026 04:50
@itomek itomek mentioned this pull request Jul 22, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

electron Electron app changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hardware pre-flight gating + connector-on-install flow First-run onboarding wizard + in-app model download

2 participants