21 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 30-hardware-detection-mode-selection | 02 | execute | 2 |
|
|
false |
|
|
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>
- TypeScript compiles cleanly for both server and ui packages
- ModeSelector renders three cards with correct copy and selection styling
- HardwareSummaryStep shows tier-appropriate labels and privacy frame
- NexusOnboardingWizard flows through 3 steps with back navigation
- Selected mode is persisted to data/nexus-settings.json on wizard completion
- Human verification confirms visual correctness </success_criteria>