---
phase: 30-hardware-detection-mode-selection
plan: 02
type: execute
wave: 2
depends_on: ["30-01"]
files_modified:
  - ui/src/api/hardware.ts
  - ui/src/hooks/useHardwareInfo.ts
  - ui/src/components/onboarding/ModeSelector.tsx
  - ui/src/components/onboarding/HardwareSummaryStep.tsx
  - ui/src/components/NexusOnboardingWizard.tsx
autonomous: false
requirements:
  - ONBD-01
  - ONBD-02
  - ONBD-07

must_haves:
  truths:
    - "User sees hardware detection results (GPU/RAM/unified memory) during onboarding within 5 seconds"
    - "User can select mode: Personal AI Assistant, Project Builder, or Both (default pre-selected)"
    - "Selected mode is persisted via PATCH /api/nexus/settings on wizard completion"
    - "Apple Silicon shows 'Unified memory' label, never 'VRAM'"
    - "When local AI is viable (gpu or apple_silicon tier), privacy frame copy is shown: 'Local AI (recommended for privacy) - Runs entirely on your machine. No accounts. No tracking. Works offline.'"
    - "CPU-only tier shows warning: 'Slower than GPU-accelerated models - cloud AI recommended'"
    - "Wizard has 3 steps: hardware detection, mode selection, root directory (existing)"
  artifacts:
    - path: "ui/src/api/hardware.ts"
      provides: "Typed fetch wrappers for hardware probe and nexus settings"
      exports: ["fetchHardwareInfo", "fetchNexusSettings", "updateNexusSettings"]
    - path: "ui/src/hooks/useHardwareInfo.ts"
      provides: "useQuery wrapper for hardware data"
      exports: ["useHardwareInfo"]
    - path: "ui/src/components/onboarding/ModeSelector.tsx"
      provides: "Three-card mode selector with selected state styling"
      exports: ["ModeSelector"]
    - path: "ui/src/components/onboarding/HardwareSummaryStep.tsx"
      provides: "Hardware info display with skeleton loading, tier-appropriate labels, privacy frame"
      exports: ["HardwareSummaryStep"]
    - path: "ui/src/components/NexusOnboardingWizard.tsx"
      provides: "Multi-step wizard: hardware -> mode -> root directory"
  key_links:
    - from: "ui/src/hooks/useHardwareInfo.ts"
      to: "/api/system/providers"
      via: "fetch in useQuery"
      pattern: "system/providers"
    - from: "ui/src/components/onboarding/HardwareSummaryStep.tsx"
      to: "ui/src/hooks/useHardwareInfo.ts"
      via: "useHardwareInfo hook"
      pattern: "useHardwareInfo"
    - from: "ui/src/components/NexusOnboardingWizard.tsx"
      to: "ui/src/api/hardware.ts"
      via: "updateNexusSettings call on wizard complete"
      pattern: "updateNexusSettings"
    - from: "ui/src/components/NexusOnboardingWizard.tsx"
      to: "ui/src/components/onboarding/ModeSelector.tsx"
      via: "React component composition"
      pattern: "ModeSelector"
---

<objective>
Build the onboarding UI components for hardware detection display, mode selection, and wire them into the NexusOnboardingWizard as a multi-step flow.

Purpose: Delivers the user-facing experience for Phase 30 — users see their hardware, choose a mode, and the selection is persisted. This is the visual and interaction layer consuming the server endpoints from Plan 01.

Output: Four new UI files (API client, hook, ModeSelector, HardwareSummaryStep) and one modified file (NexusOnboardingWizard.tsx refactored to 3-step flow).
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/30-hardware-detection-mode-selection/30-RESEARCH.md
@.planning/phases/30-hardware-detection-mode-selection/30-UI-SPEC.md
@.planning/phases/30-hardware-detection-mode-selection/30-01-SUMMARY.md

@ui/src/components/NexusOnboardingWizard.tsx
@ui/src/api/client.ts
@ui/src/lib/queryKeys.ts
@ui/src/lib/utils.ts
@ui/src/components/ui/skeleton.tsx
@ui/src/components/ui/button.tsx

<interfaces>
<!-- Server endpoints created by Plan 01 -->

From server/src/services/hardware.ts:
```typescript
export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";
export interface HardwareInfo {
  totalGb: number;
  freeGb: number;
  usableGb: number;
  platform: NodeJS.Platform;
  gpuName: string | null;
  gpuVramGb: number | null;
  unifiedMemory: boolean;
  hardwareTier: HardwareTier;
  cpuModel: string | null;
}
```

From server/src/services/nexus-settings.ts:
```typescript
export const NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const;
export type NexusMode = (typeof NEXUS_MODES)[number];
export type NexusSettings = { mode: NexusMode };
```

Server endpoints:
- GET /api/system/providers -> HardwareInfo (unauthenticated)
- GET /api/nexus/settings -> NexusSettings (board auth)
- PATCH /api/nexus/settings -> NexusSettings (board auth, body: Partial of NexusSettings)

From ui/src/api/client.ts:
```typescript
// Simple fetch wrapper — all API modules use this pattern:
// import { api } from "./client";
// const data = await api.get("/endpoint");
// const data = await api.patch("/endpoint", body);
```

From ui/src/lib/queryKeys.ts:
```typescript
export const queryKeys = {
  // ... existing keys
  // Add: hardware: { info: ["hardware", "info"] as const }
};
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: API client, hook, ModeSelector, and HardwareSummaryStep</name>
  <files>
    ui/src/api/hardware.ts
    ui/src/hooks/useHardwareInfo.ts
    ui/src/components/onboarding/ModeSelector.tsx
    ui/src/components/onboarding/HardwareSummaryStep.tsx
    ui/src/lib/queryKeys.ts
  </files>
  <read_first>
    ui/src/api/client.ts
    ui/src/api/agents.ts
    ui/src/hooks/useHardwareInfo.ts
    ui/src/lib/queryKeys.ts
    ui/src/lib/utils.ts
    ui/src/components/ui/skeleton.tsx
    .planning/phases/30-hardware-detection-mode-selection/30-UI-SPEC.md
    .planning/phases/30-hardware-detection-mode-selection/30-RESEARCH.md
  </read_first>
  <action>
    **1. Create `ui/src/api/hardware.ts`:**

    Define types locally (not imported from server — UI and server are separate packages):
    ```typescript
    export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";

    export interface HardwareInfo {
      totalGb: number;
      freeGb: number;
      usableGb: number;
      platform: string;
      gpuName: string | null;
      gpuVramGb: number | null;
      unifiedMemory: boolean;
      hardwareTier: HardwareTier;
      cpuModel: string | null;
    }

    export type NexusMode = "personal_ai" | "project_builder" | "both";
    export interface NexusSettings { mode: NexusMode; }
    ```

    Export three functions using the `api` client from `"./client"`:
    - `fetchHardwareInfo(): Promise<HardwareInfo>` — `api.get("/system/providers")`
    - `fetchNexusSettings(): Promise<NexusSettings>` — `api.get("/nexus/settings")`
    - `updateNexusSettings(settings: Partial<NexusSettings>): Promise<NexusSettings>` — `api.patch("/nexus/settings", settings)`

    **2. Update `ui/src/lib/queryKeys.ts`:**

    Add to the queryKeys object:
    ```typescript
    hardware: {
      info: ["hardware", "info"] as const,
    },
    ```

    **3. Create `ui/src/hooks/useHardwareInfo.ts`:**

    ```typescript
    import { useQuery } from "@tanstack/react-query";
    import { fetchHardwareInfo, type HardwareInfo } from "../api/hardware";
    import { queryKeys } from "../lib/queryKeys";

    export function useHardwareInfo(enabled = true) {
      return useQuery<HardwareInfo>({
        queryKey: queryKeys.hardware.info,
        queryFn: fetchHardwareInfo,
        enabled,
        staleTime: 5 * 60 * 1000, // 5 minutes — matches server cache TTL
        retry: 1,
      });
    }
    ```

    **4. Create `ui/src/components/onboarding/ModeSelector.tsx`:**

    Exact implementation from RESEARCH.md Pattern 5 and UI-SPEC.md:
    - Three buttons in a vertical grid (`grid gap-3`).
    - Each button: `flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors`.
    - Selected state: `border-primary bg-primary/5`.
    - Unselected state: `border-border hover:border-muted-foreground/50`.
    - Label: `font-medium text-sm`. Description: `text-xs text-muted-foreground`.

    Mode definitions (verbatim from PRD/RESEARCH):
    ```
    personal_ai: "Personal AI Assistant" / "Always available, persistent memory, private."
    project_builder: "Project Builder" / "Brainstorm -> PM -> Engineer -> shipped product."
    both: "Both (recommended)" / "A conversation becomes a project with one click."
    ```

    Props: `{ value: NexusMode; onChange: (mode: NexusMode) => void }`.
    Import `cn` from `"@/lib/utils"` and `NexusMode` type from `"@/api/hardware"`.

    **5. Create `ui/src/components/onboarding/HardwareSummaryStep.tsx`:**

    Props: `{ hardwareInfo: HardwareInfo | undefined; isLoading: boolean; isError: boolean }`.

    **Loading state** (isLoading = true): Render three `Skeleton` rows (`h-4 w-full rounded`). Import Skeleton from `"@/components/ui/skeleton"`.

    **Error state** (isError = true): Render `<p className="text-sm text-muted-foreground">Could not detect hardware. You can still continue.</p>`. Note: NOT `text-destructive` — this is non-blocking per UI-SPEC.

    **Success state** (hardwareInfo exists):

    Render a vertical stack (`flex flex-col gap-4`):

    a) Hardware stats rows (`flex flex-col gap-2`):
    - If `hardwareInfo.hardwareTier === "apple_silicon"`:
      - Row: label "Unified memory" (never "VRAM"), value `{hardwareInfo.totalGb} GB`
      - Row: label "Available", value `{hardwareInfo.usableGb} GB`
      - Row: label "CPU", value `{hardwareInfo.cpuModel}`
    - If `hardwareInfo.hardwareTier === "gpu"`:
      - Row: label "GPU", value `{hardwareInfo.gpuName}`
      - Row: label "GPU VRAM", value `{hardwareInfo.gpuVramGb} GB`
      - Row: label "System RAM", value `{hardwareInfo.totalGb} GB`
    - If `hardwareInfo.hardwareTier === "cpu_only"`:
      - Row: label "System RAM", value `{hardwareInfo.totalGb} GB`
      - Row: label "CPU", value `{hardwareInfo.cpuModel}`
      - Warning: `<p className="text-xs text-muted-foreground">Slower than GPU-accelerated models -- cloud AI recommended</p>`

    Each stat row: `<div className="flex items-center justify-between gap-2"><span className="text-xs text-muted-foreground">{label}</span><span className="text-sm font-medium">{value}</span></div>`

    b) Privacy frame (shown when `hardwareTier !== "cpu_only"`):
    ```tsx
    <div className="flex flex-col gap-1 pt-2">
      <span className="text-sm font-medium">Local AI (recommended for privacy)</span>
      <span className="text-xs text-muted-foreground">
        Runs entirely on your machine.{"\n"}
        No accounts. No tracking. Works offline.
      </span>
    </div>
    ```

    Import `cn` from `"@/lib/utils"`, `Skeleton` from `"@/components/ui/skeleton"`, `HardwareInfo` from `"@/api/hardware"`.
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - ui/src/api/hardware.ts exports `fetchHardwareInfo`, `fetchNexusSettings`, `updateNexusSettings`, `HardwareInfo`, `HardwareTier`, `NexusMode`, `NexusSettings`
    - ui/src/api/hardware.ts contains `/system/providers`
    - ui/src/api/hardware.ts contains `/nexus/settings`
    - ui/src/hooks/useHardwareInfo.ts exports `useHardwareInfo`
    - ui/src/hooks/useHardwareInfo.ts contains `queryKeys.hardware.info`
    - ui/src/lib/queryKeys.ts contains `hardware:` key with `info:` subkey
    - ui/src/components/onboarding/ModeSelector.tsx exports `ModeSelector`
    - ui/src/components/onboarding/ModeSelector.tsx contains `"Personal AI Assistant"`
    - ui/src/components/onboarding/ModeSelector.tsx contains `"Project Builder"`
    - ui/src/components/onboarding/ModeSelector.tsx contains `"Both (recommended)"`
    - ui/src/components/onboarding/ModeSelector.tsx contains `border-primary bg-primary/5`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx exports `HardwareSummaryStep`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `"Unified memory"` (for Apple Silicon)
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `"Local AI (recommended for privacy)"`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `"Runs entirely on your machine"`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `"Slower than GPU-accelerated models"`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `"Could not detect hardware. You can still continue."`
    - ui/src/components/onboarding/HardwareSummaryStep.tsx contains `Skeleton` import
    - TypeScript compilation exits 0 with no errors
  </acceptance_criteria>
  <done>All four new UI files created with correct types, copy, and styling. ModeSelector shows three cards with correct labels and selection state. HardwareSummaryStep shows tier-appropriate hardware info, privacy frame for local AI tiers, and warning for CPU-only. TypeScript compiles cleanly.</done>
</task>

<task type="auto">
  <name>Task 2: Wire multi-step wizard in NexusOnboardingWizard</name>
  <files>
    ui/src/components/NexusOnboardingWizard.tsx
  </files>
  <read_first>
    ui/src/components/NexusOnboardingWizard.tsx
    ui/src/components/onboarding/ModeSelector.tsx
    ui/src/components/onboarding/HardwareSummaryStep.tsx
    ui/src/hooks/useHardwareInfo.ts
    ui/src/api/hardware.ts
    .planning/phases/30-hardware-detection-mode-selection/30-UI-SPEC.md
  </read_first>
  <action>
    Refactor `NexusOnboardingWizard.tsx` from a single-step form into a 3-step wizard.

    **Step state:** Add `const [step, setStep] = useState(1);` — values 1, 2, 3.
    - Step 1: Hardware detection (auto-runs, no user input) — shows `HardwareSummaryStep`
    - Step 2: Mode selection — shows `ModeSelector`
    - Step 3: Root directory (existing form) — shows existing Input + submit button

    **Mode state:** Add `const [selectedMode, setSelectedMode] = useState<NexusMode>("both");` — default "both" as per UI-SPEC ("Both (recommended)" pre-selected on mount).

    **Hardware hook:** Add `const { data: hardwareInfo, isLoading: hwLoading, isError: hwError } = useHardwareInfo(effectiveOnboardingOpen);` — only fetch when wizard is open.

    **Step indicator:** Above the step content, render:
    ```tsx
    <p className="text-xs text-muted-foreground text-center">Step {step} of 3</p>
    ```

    **Step 1 — Hardware Detection:**
    - Heading: `hwLoading ? "Detecting your hardware..." : "Your hardware"` (text-2xl font-semibold, per UI-SPEC)
    - Body: `<HardwareSummaryStep hardwareInfo={hardwareInfo} isLoading={hwLoading} isError={hwError} />`
    - Button: "Continue" — always enabled (hardware probe is non-blocking). On click: `setStep(2)`.

    **Step 2 — Mode Selection:**
    - Heading: "Choose your mode" (text-2xl font-semibold)
    - Body: `<ModeSelector value={selectedMode} onChange={setSelectedMode} />`
    - Button: "Continue" — always enabled. On click: `setStep(3)`.

    **Step 3 — Root Directory:**
    - Heading: keep existing `Welcome to {VOCAB.appName}` heading and description text (adapter-dependent copy)
    - Body: keep existing Input field for rootDir
    - Button: keep existing "Get Started" button and submit logic

    **On submit (handleSubmit):** After the existing company + agent creation logic succeeds, add a call to persist the selected mode:
    ```typescript
    // Persist selected mode
    try {
      await updateNexusSettings({ mode: selectedMode });
    } catch {
      // Non-blocking — mode defaults to "both" if save fails
    }
    ```

    Place this AFTER the company creation succeeds but BEFORE the navigate call. Import `updateNexusSettings` from `"@/api/hardware"`.

    **Back navigation:** On steps 2 and 3, show a secondary "Back" button (variant="ghost") that decrements step. No back button on step 1.

    **Reset:** In the existing reset effect (when wizard closes), also reset `step` to 1 and `selectedMode` to "both".

    **Imports to add:**
    ```typescript
    import { ModeSelector } from "./onboarding/ModeSelector";
    import { HardwareSummaryStep } from "./onboarding/HardwareSummaryStep";
    import { useHardwareInfo } from "../hooks/useHardwareInfo";
    import { updateNexusSettings, type NexusMode } from "../api/hardware";
    ```

    **Preserve:** All existing adapter probe logic (Hermes detection), the Dialog/DialogPortal structure, the form submission flow, and the handleClose function. The wizard card's outer styling (`p-8 flex flex-col gap-6`, max-w-md, shadow-2xl) must remain unchanged.

    **Key constraint:** The existing export must remain `export function OnboardingWizard()` — this is the named export consumed by App.tsx via the Vite alias.
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - ui/src/components/NexusOnboardingWizard.tsx contains `import { ModeSelector }` from `"./onboarding/ModeSelector"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `import { HardwareSummaryStep }` from `"./onboarding/HardwareSummaryStep"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `import { useHardwareInfo }` from `"../hooks/useHardwareInfo"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `import { updateNexusSettings` from `"../api/hardware"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `useState(1)` for step state
    - ui/src/components/NexusOnboardingWizard.tsx contains `useState.*"both"` for mode state
    - ui/src/components/NexusOnboardingWizard.tsx contains `Step {step} of 3` or `Step ${step} of 3`
    - ui/src/components/NexusOnboardingWizard.tsx contains `"Detecting your hardware"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `"Your hardware"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `"Choose your mode"`
    - ui/src/components/NexusOnboardingWizard.tsx contains `updateNexusSettings({ mode: selectedMode })`
    - ui/src/components/NexusOnboardingWizard.tsx contains `export function OnboardingWizard()`
    - TypeScript compilation exits 0 with no errors
  </acceptance_criteria>
  <done>NexusOnboardingWizard is a 3-step wizard (hardware detection, mode selection, root directory). Step indicator shows current step. Back button works on steps 2-3. Mode is persisted on completion. All existing functionality preserved.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 3: Visual verification of onboarding wizard flow</name>
  <files>ui/src/components/NexusOnboardingWizard.tsx</files>
  <action>
    Human verifies the complete 3-step onboarding wizard flow by running the dev server and walking through each step visually.
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter ui exec tsc --noEmit</automated>
  </verify>
  <what-built>
    Three-step onboarding wizard: hardware detection display, mode selector cards, and root directory input. Hardware probe runs automatically and shows GPU/RAM/unified memory info. Mode selector has three cards (Personal AI Assistant, Project Builder, Both) with "Both" pre-selected. Privacy copy shown for local-AI-capable hardware.
  </what-built>
  <how-to-verify>
    1. Start the dev server: `cd /opt/nexus && pnpm dev`
    2. Open browser to http://localhost:3100
    3. The onboarding wizard should appear (or trigger it by navigating to /onboarding)
    4. **Step 1 — Hardware Detection:**
       - Verify skeleton loading state briefly appears
       - Verify hardware info renders (RAM, GPU/unified memory as appropriate)
       - If on a machine with GPU or Apple Silicon: verify "Local AI (recommended for privacy)" copy appears
       - If CPU-only: verify "Slower than GPU-accelerated models" warning appears
       - Click "Continue"
    5. **Step 2 — Mode Selection:**
       - Verify "Choose your mode" heading
       - Verify "Both (recommended)" is pre-selected with blue border
       - Click a different mode — verify selection moves
       - Verify step indicator shows "Step 2 of 3"
       - Click "Continue"
    6. **Step 3 — Root Directory:**
       - Verify existing root directory input appears
       - Verify "Back" button takes you back to step 2
       - Complete the form and submit
    7. Verify the wizard closes and navigates to dashboard
  </how-to-verify>
  <resume-signal>Type "approved" or describe issues</resume-signal>
  <done>Human confirms wizard renders correctly across all 3 steps with proper copy, styling, and navigation.</done>
</task>

</tasks>

<verification>
TypeScript compilation:
```bash
cd /opt/nexus && pnpm --filter ui exec tsc --noEmit
```

Server tests:
```bash
cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection
```

Dev server runs without errors:
```bash
cd /opt/nexus && pnpm dev
```
</verification>

<success_criteria>
1. TypeScript compiles cleanly for both server and ui packages
2. ModeSelector renders three cards with correct copy and selection styling
3. HardwareSummaryStep shows tier-appropriate labels and privacy frame
4. NexusOnboardingWizard flows through 3 steps with back navigation
5. Selected mode is persisted to data/nexus-settings.json on wizard completion
6. Human verification confirms visual correctness
</success_criteria>

<output>
After completion, create `.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md`
</output>
