nexus/.planning/phases/30-hardware-detection-mode-selection/30-02-PLAN.md
2026-04-04 03:55:49 +00:00

21 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
30-hardware-detection-mode-selection 02 execute 2
30-01
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
false
ONBD-01
ONBD-02
ONBD-07
truths artifacts key_links
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)
path provides exports
ui/src/api/hardware.ts Typed fetch wrappers for hardware probe and nexus settings
fetchHardwareInfo
fetchNexusSettings
updateNexusSettings
path provides exports
ui/src/hooks/useHardwareInfo.ts useQuery wrapper for hardware data
useHardwareInfo
path provides exports
ui/src/components/onboarding/ModeSelector.tsx Three-card mode selector with selected state styling
ModeSelector
path provides exports
ui/src/components/onboarding/HardwareSummaryStep.tsx Hardware info display with skeleton loading, tier-appropriate labels, privacy frame
HardwareSummaryStep
path provides
ui/src/components/NexusOnboardingWizard.tsx Multi-step wizard: hardware -> mode -> root directory
from to via pattern
ui/src/hooks/useHardwareInfo.ts /api/system/providers fetch in useQuery system/providers
from to via pattern
ui/src/components/onboarding/HardwareSummaryStep.tsx ui/src/hooks/useHardwareInfo.ts useHardwareInfo hook useHardwareInfo
from to via pattern
ui/src/components/NexusOnboardingWizard.tsx ui/src/api/hardware.ts updateNexusSettings call on wizard complete updateNexusSettings
from to via pattern
ui/src/components/NexusOnboardingWizard.tsx ui/src/components/onboarding/ModeSelector.tsx React component composition ModeSelector
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).

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From server/src/services/hardware.ts:

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:

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:

// 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:

export const queryKeys = {
  // ... existing keys
  // Add: hardware: { info: ["hardware", "info"] as const }
};
Task 1: API client, hook, ModeSelector, and HardwareSummaryStep 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 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 **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"`.
cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -30 - 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 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. Task 2: Wire multi-step wizard in NexusOnboardingWizard ui/src/components/NexusOnboardingWizard.tsx 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 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.
cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 2>&1 | head -30 - 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 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. Task 3: Visual verification of onboarding wizard flow ui/src/components/NexusOnboardingWizard.tsx Human verifies the complete 3-step onboarding wizard flow by running the dev server and walking through each step visually. cd /opt/nexus && pnpm --filter ui exec tsc --noEmit 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. 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 Type "approved" or describe issues Human confirms wizard renders correctly across all 3 steps with proper copy, styling, and navigation. TypeScript compilation: ```bash cd /opt/nexus && pnpm --filter ui exec tsc --noEmit ```

Server tests:

cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection

Dev server runs without errors:

cd /opt/nexus && pnpm dev

<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>
After completion, create `.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md`