nexus/.planning/phases/32-multi-step-onboarding-wizard/32-RESEARCH.md

20 KiB

Phase 32: Multi-Step Onboarding Wizard - Research

Researched: 2026-04-01 Domain: React wizard UI — step navigation, summary screen, skip flow, chat handoff Confidence: HIGH

Summary

Phase 32 completes the onboarding wizard that Phases 30 and 31 built. The wizard already has 4 steps (hardware detection, mode selection, provider selection, root directory). This phase adds three things: (1) a summary screen that replaces or follows the root directory step to show the user what they configured, (2) a guaranteed skip path through every step, and (3) a "Go to chat" button on the summary screen that opens the chat panel and closes the wizard without waiting.

The skip mechanism already partially exists — ProviderSelectionStep has a "Skip for now" ghost button. What's missing is: (a) consistent skip affordance on steps 1 and 2, (b) a full summary step (step 5) that displays hardware tier, selected mode, and chosen provider, and (c) a one-click path from summary to chat using useChatPanel().setChatOpen(true).

No server-side changes are needed. Voice options (Whisper STT) are already wired server-side via POST /api/transcribe in chat-files.ts but there is no onboarding step for them. VOICE-03 is assigned to Phase 34, not Phase 32. Phase 32 should not introduce voice configuration UI — that is out of scope here.

Primary recommendation: Add a OnboardingSummaryStep component in ui/src/components/onboarding/, insert it as step 5 in NexusOnboardingWizard.tsx, add skip buttons to steps 1 and 2, and wire the summary's "Start chatting" CTA to setChatOpen(true) from useChatPanel.

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.

Claude's Discretion

All implementation choices.

Deferred Ideas (OUT OF SCOPE)

None — discuss phase skipped. </user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
ONBD-04 User can skip any onboarding step without blocking subsequent steps Steps 1 and 2 have no skip button today; step 3 has "Skip for now" ghost button; pattern is established — replicate to all steps
ONBD-05 User sees summary screen showing configured providers and agent-model pairings New OnboardingSummaryStep component needed as step 5; data available in wizard state (hardwareInfo, selectedMode, puterToken/googleOAuthStateId/apiKeyData)
ONBD-06 User can go from summary screen directly into chat with one click useChatPanel().setChatOpen(true) + closeOnboarding() + navigate(/${company.issuePrefix}/dashboard) — but this requires company creation first
</phase_requirements>

Standard Stack

Core

Library Version Purpose Why Standard
React 18.x (project baseline) Wizard component state Already in use throughout
@tanstack/react-query 5.x (project baseline) Hardware info query useHardwareInfo hook already uses it
react-router-dom (via @/lib/router) project alias Navigation after wizard completion Already used in wizard

Supporting

Library Version Purpose When to Use
lucide-react project baseline Icons in summary screen All existing onboarding components use it
@/components/ui/button project baseline Consistent button styling All CTA buttons in wizard use it
cn from @/lib/utils project baseline Conditional class merging Used in all onboarding sub-components

Installation: No new packages. All dependencies already installed.

Architecture Patterns

ui/src/components/
├── NexusOnboardingWizard.tsx    # main wizard — add step 5 (summary), skip to steps 1+2
└── onboarding/
    ├── HardwareSummaryStep.tsx  # existing (step 1 display)
    ├── ModeSelector.tsx         # existing (step 2)
    ├── ProviderSelectionStep.tsx # existing (step 3) — already has skip
    ├── ApiKeyEntryForm.tsx      # existing
    ├── GoogleOAuthButton.tsx    # existing
    ├── PuterAuthButton.tsx      # existing
    └── OnboardingSummaryStep.tsx # NEW — step 5

Pattern 1: Step State Machine

What: Integer step state (1-4 today, will become 1-5). Each step renders conditionally on {step === N}. Navigation via setStep(N). When to use: Already the established pattern — do not change to a different mechanism. Example:

// From NexusOnboardingWizard.tsx (existing pattern)
const [step, setStep] = useState(1);
// ...
{step === 5 && (
  <OnboardingSummaryStep
    hardwareInfo={hardwareInfo}
    selectedMode={selectedMode}
    providerChosen={providerChosen}
    onStartChat={handleStartChat}
    onBack={() => setStep(4)}
  />
)}

Pattern 2: Skip Button (ghost variant)

What: Every step except the last (summary) must have a skip CTA. Current pattern from ProviderSelectionStep: <Button variant="ghost" onClick={onSkip}>Skip for now</Button>. When to use: Steps 1, 2, and 4 need skip added. Step 3 already has it. Example:

// Step 1 — hardware detection, add after Continue button:
<Button type="button" variant="ghost" onClick={() => setStep(2)} className="w-full">
  Skip
</Button>
// Step 2 — mode selection, add after Back/Continue buttons:
<Button type="button" variant="ghost" onClick={() => setStep(3)} className="w-full">
  Skip
</Button>
// Step 4 — root directory, add after Back button:
<Button type="button" variant="ghost" onClick={() => setStep(5)} className="w-full" disabled={loading}>
  Skip to summary
</Button>

Pattern 3: Jump to Chat (ONBD-06)

What: On the summary screen, "Start chatting" CTA calls setChatOpen(true) then calls handleSubmit logic (or equivalent). The key insight: company creation must happen before navigating to chat. The summary step is shown after step 4 (root directory) where the user may or may not have entered a directory. The "Start chatting" button must trigger company creation if it hasn't happened yet. When to use: Summary step's primary CTA.

Two design options:

Option A (simpler): Keep the existing handleSubmit on step 4, make "Get Started" button advance to step 5 (summary) instead of submitting, then summary's "Start chatting" actually submits. Company creation happens when the user clicks "Start chatting".

Option B: Move company creation to wizard close, make step 5 a pure display step, "Start chatting" closes wizard and opens chat.

Recommendation: Option A. The summary step is after all configuration is complete; when user clicks "Start chatting" on the summary, call the existing handleSubmit logic plus setChatOpen(true) afterward.

// In NexusOnboardingWizard.tsx
import { useChatPanel } from "../context/ChatPanelContext";
// ...
const { setChatOpen } = useChatPanel();

async function handleStartChat(e?: React.FormEvent) {
  // Same company creation logic as handleSubmit
  // After navigate(/${company.issuePrefix}/dashboard):
  setChatOpen(true);
}

Pattern 4: OnboardingSummaryStep Component

What: A read-only summary card showing: hardware tier + memory, selected mode, provider name (or "None selected"). When to use: Step 5 of the wizard. Props:

interface OnboardingSummaryStepProps {
  hardwareInfo: HardwareInfo | undefined;
  selectedMode: NexusMode;
  providerChosen: "puter" | "google" | "apikey" | null;
  rootDir: string;
  loading: boolean;
  error: string | null;
  onStartChat: () => void;
  onBack: () => void;
}

Anti-Patterns to Avoid

  • Changing the step integer type to a string enum: The existing step: 1 | 2 | 3 | 4 pattern is simple and consistent — keep it as 1 | 2 | 3 | 4 | 5.
  • Moving company creation earlier (before summary): Company creation must stay in handleSubmit — called on "Start chatting" click. Creating the company before the user finalizes breaks the established pattern from Phase 31.
  • Calling setChatOpen(true) before navigate(): The chat panel is rendered inside Layout, which only renders after navigation to a company route. Call setChatOpen(true) after navigate().
  • Adding voice step in Phase 32: VOICE-03 is Phase 34. Do not add a voice config step now.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Provider display name Custom string map Inline label constants Simple enough for a const PROVIDER_LABELS in the summary component
Chat panel open state Custom context/ref useChatPanel().setChatOpen Already wired to localStorage persistence and Layout rendering
Hardware tier label Custom detection Read from hardwareInfo.hardwareTier Already computed by useHardwareInfo hook
Step validation Custom state machine Simple setStep(N) calls 5 linear steps, no branching beyond skip

Key insight: This phase is pure UI composition. All data is already in wizard state; no new APIs, hooks, or server routes are needed.

Runtime State Inventory

Not applicable. This is a greenfield UI-only change — no rename, refactor, or migration involved.

Common Pitfalls

Pitfall 1: Step Count Mismatch in UI

What goes wrong: The wizard shows "Step X of 4" in the header but there are now 5 steps. Why it happens: <p className="text-xs text-muted-foreground text-center">Step {step} of 4</p> is hardcoded. How to avoid: Change 4 to 5 in the step indicator string when adding step 5. Also suppress "Step 5 of 5" on the summary — show "Summary" or "Almost done" instead since it's not a data entry step. Warning signs: QA screenshot shows "Step 5 of 4".

Pitfall 2: setChatOpen(true) Before Company Route Exists

What goes wrong: Chat panel tries to render in a context without a selectedCompanyId, showing an empty state or an error. Why it happens: ChatPanel uses useChatPanel() and useCompany(). The company only exists after companiesApi.create() and setSelectedCompanyId(). How to avoid: Call setChatOpen(true) after navigate(\/${company.issuePrefix}/dashboard`)insidehandleStartChat`. React Router navigation is synchronous at the call site — Layout re-renders with the new company before the next paint. Warning signs: Chat panel opens but shows no conversation list.

Pitfall 3: Summary Step Not Receiving rootDir Data

What goes wrong: Summary screen shows empty root directory field, even though user entered one. Why it happens: rootDir state is in the wizard parent but not passed to OnboardingSummaryStep. How to avoid: Pass rootDir as a prop. The component should display it in a font-mono text-sm span (matching the existing Input style). Warning signs: TypeScript error on prop type mismatch, or blank field in summary.

Pitfall 4: Skip on Step 4 Skips Company Creation

What goes wrong: User skips the root directory step → summary appears → user clicks "Start chatting" → submit runs with empty rootDir → fails for claude_local adapter (which requires a cwd). Why it happens: handleSubmit has if (defaultAdapter === "claude_local" && !rootDir.trim()) return; guard. How to avoid: The submit logic already handles this correctly — it returns early without creating a company. The "Start chatting" button on the summary must be the same submit function. If defaultAdapter === "claude_local" and rootDir is empty, show the error on the summary screen. Alternatively, grey out "Start chatting" with a tooltip "Root directory required for Claude Code". Warning signs: User clicks "Start chatting" and nothing happens (silent return in handleSubmit).

Pitfall 5: Provider Label for No-Provider Case

What goes wrong: Summary screen shows null or undefined when no provider was selected. Why it happens: puterToken, googleOAuthStateId, and apiKeyData are all null when user skipped step 3. How to avoid: Show "No provider selected — you can add one later in Settings" when all three are null. Warning signs: Summary shows blank provider row or crashes on null access.

Code Examples

Reading Provider State for Summary Display

// Source: NexusOnboardingWizard.tsx (existing state)
// Determine which provider was configured
function deriveProviderLabel(
  puterToken: string | null,
  googleOAuthStateId: string | null,
  apiKeyData: { provider: string; apiKey: string } | null,
): string {
  if (puterToken) return "Puter (free, zero-config)";
  if (googleOAuthStateId) return "Google Gemini (free tier)";
  if (apiKeyData) return `API key — ${apiKeyData.provider}`;
  return "None selected";
}

Opening Chat After Wizard Completion

// Source: ChatPanelContext.tsx — setChatOpen persists to localStorage
const { setChatOpen } = useChatPanel();

// Inside handleStartChat, after navigate():
navigate(`/${company.issuePrefix}/dashboard`);
setChatOpen(true);
closeOnboarding();

Summary Step Skeleton

// ui/src/components/onboarding/OnboardingSummaryStep.tsx
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { HardwareInfo } from "@/api/hardware";
import type { NexusMode } from "@/api/hardware";

interface OnboardingSummaryStepProps {
  hardwareInfo: HardwareInfo | undefined;
  selectedMode: NexusMode;
  providerLabel: string;
  rootDir: string;
  loading: boolean;
  error: string | null;
  onStartChat: () => void;
  onBack: () => void;
}

export function OnboardingSummaryStep({
  hardwareInfo, selectedMode, providerLabel, rootDir, loading, error, onStartChat, onBack,
}: OnboardingSummaryStepProps) {
  const modeLabels: Record<NexusMode, string> = {
    personal_ai: "Personal AI Assistant",
    project_builder: "Project Builder",
    both: "Both (recommended)",
  };
  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-col gap-2 rounded-lg border border-border p-4 text-sm">
        <SummaryRow label="Hardware" value={hardwareInfo?.hardwareTier ?? "Unknown"} />
        <SummaryRow label="Mode" value={modeLabels[selectedMode]} />
        <SummaryRow label="Provider" value={providerLabel} />
        {rootDir && <SummaryRow label="Root directory" value={rootDir} mono />}
      </div>
      {error && (
        <p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">{error}</p>
      )}
      <Button type="button" onClick={onStartChat} disabled={loading} className="w-full">
        {loading ? "Setting up…" : "Start chatting"}
      </Button>
      <Button type="button" variant="ghost" onClick={onBack} className="w-full" disabled={loading}>
        Back
      </Button>
    </div>
  );
}

State of the Art

Old Approach Current Approach When Changed Impact
4-step wizard (hw, mode, provider, rootDir) 5-step wizard adding summary Phase 32 No breaking changes — just append step 5
No skip on steps 1 and 2 Skip button on all steps Phase 32 Users who skip all still need one valid agent — see PROJECT.md constraint

Key constraint from PROJECT.md (established in roadmap):

Skip-all minimum valid state: one working agent with a valid provider must be created when user skips all steps.

This means when the user skips everything and clicks "Start chatting" on the summary: the wizard must still call handleSubmit (which creates the company + agents). The default adapter (claude_local or hermes_local) and optional rootDir must still produce a valid state. For hermes_local this is fine (rootDir optional). For claude_local with no rootDir — the submit guard returns early. The UX must communicate this: disable "Start chatting" or show inline error on the summary if claude_local adapter is selected and rootDir is empty.

Environment Availability

Step 2.6: SKIPPED — this phase is UI-only React changes. No new external dependencies.

Validation Architecture

Test Framework

Property Value
Framework Vitest + React Testing Library (project standard)
Config file ui/vitest.config.ts
Quick run command pnpm vitest run ui/src/components/onboarding/
Full suite command pnpm test:run

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
ONBD-04 Skip button present on all steps unit pnpm vitest run ui/src/components/NexusOnboardingWizard.test.tsx Wave 0
ONBD-05 Summary screen renders hardware/mode/provider data unit pnpm vitest run ui/src/components/onboarding/OnboardingSummaryStep.test.tsx Wave 0
ONBD-06 "Start chatting" calls setChatOpen(true) and navigate unit pnpm vitest run ui/src/components/NexusOnboardingWizard.test.tsx Wave 0

Sampling Rate

  • Per task commit: pnpm vitest run ui/src/components/onboarding/
  • Per wave merge: pnpm test:run
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • ui/src/components/onboarding/OnboardingSummaryStep.test.tsx — covers ONBD-05
  • ui/src/components/NexusOnboardingWizard.test.tsx — covers ONBD-04, ONBD-06

Open Questions

  1. Should step 4 (root directory) survive or be merged into summary?

    • What we know: Step 4 is the only form input step. Summary (step 5) is read-only except for the submit CTA.
    • What's unclear: Whether the root directory input belongs on the summary screen or stays as its own step.
    • Recommendation: Keep step 4 as a separate form step; summary is step 5 (pure read-only). This is less disruptive to existing code.
  2. "Start chatting" on summary — does it also navigate or only open chat panel?

    • What we know: Current wizard navigates to /${company.issuePrefix}/dashboard on completion.
    • What's unclear: The requirement says "go directly into chat" — does this mean open the chat panel on dashboard, or navigate to a chat-specific route?
    • Recommendation: Navigate to dashboard + setChatOpen(true). The chat panel slides in from the right on all company pages. No separate chat route exists.
  3. Step indicator label for summary step

    • What we know: Current indicator is "Step N of 4" — hardcoded.
    • Recommendation: Show "Summary" instead of "Step 5 of 5" on step 5 to signal it's a review step, not a data entry step.

Sources

Primary (HIGH confidence)

  • Direct codebase inspection: ui/src/components/NexusOnboardingWizard.tsx — full wizard implementation
  • Direct codebase inspection: ui/src/components/onboarding/*.tsx — all 6 sub-components
  • Direct codebase inspection: ui/src/context/ChatPanelContext.tsx — setChatOpen API
  • Direct codebase inspection: server/src/routes/chat-files.ts — Whisper transcription (Phase 34 only)
  • Direct codebase inspection: .planning/STATE.md — Project constraints (skip-all minimum valid state)
  • Direct codebase inspection: .planning/REQUIREMENTS.md — ONBD-04/05/06 exact wording

Secondary (MEDIUM confidence)

  • None needed — all required information in codebase.

Tertiary (LOW confidence)

  • None.

Metadata

Confidence breakdown:

  • Standard stack: HIGH — all existing code confirmed
  • Architecture: HIGH — wizard pattern is established, summary step is additive
  • Pitfalls: HIGH — derived from direct reading of existing component code and constraint documentation

Research date: 2026-04-01 Valid until: 2026-05-01 (stable React codebase, no moving dependencies)