diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index df8bff16..946a1b3b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -108,7 +108,11 @@ Plans: 2. A Mac Mini M4 reports "unified memory" (not VRAM) with the 0.75 multiplier applied and copy that says "runs entirely on your machine" 3. The mode selector (Personal AI Assistant / Project Builder / Both) is visible during onboarding and the selected mode is persisted; assistant-specific UI is hidden when Project Builder-only is chosen 4. The model recommendation shown to the user matches an entry in the pre-built JSON catalog for the detected hardware tier (GPU / Apple Silicon / CPU-only) -**Plans**: TBD +**Plans**: 2 plans + +Plans: +- [ ] 30-01-PLAN.md — Hardware service, nexus-settings service, model catalog extension, routes, and tests +- [ ] 30-02-PLAN.md — ModeSelector, HardwareSummaryStep, useHardwareInfo hook, multi-step wizard wiring ### Phase 31: Puter.js Zero-Config Cloud **Goal**: Users without Ollama installed can reach working AI in one click via Puter.js — all calls server-proxied, tokens server-stored, cost tracked; Google OAuth and subscription auto-detection round out the provider tier @@ -211,7 +215,7 @@ All 21 v1.5 requirements are mapped to exactly one phase. No orphans. | 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 | | 28. Ollama Integration & Agent Surface | v1.4 | 3/3 | Complete | 2026-04-02 | | 29. Default Provider & End-to-End | v1.4 | 2/2 | Complete | 2026-04-02 | -| 30. Hardware Detection + Mode Selection | v1.5 | 0/TBD | Not started | - | +| 30. Hardware Detection + Mode Selection | v1.5 | 0/2 | Planning | - | | 31. Puter.js Zero-Config Cloud | v1.5 | 0/TBD | Not started | - | | 32. Multi-Step Onboarding Wizard | v1.5 | 0/TBD | Not started | - | | 33. Persistent Memory + Personal Assistant Mode | v1.5 | 0/TBD | Not started | - | diff --git a/.planning/phases/30-hardware-detection-mode-selection/30-01-PLAN.md b/.planning/phases/30-hardware-detection-mode-selection/30-01-PLAN.md new file mode 100644 index 00000000..f5b47eac --- /dev/null +++ b/.planning/phases/30-hardware-detection-mode-selection/30-01-PLAN.md @@ -0,0 +1,382 @@ +--- +phase: 30-hardware-detection-mode-selection +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - server/src/services/hardware.ts + - server/src/services/nexus-settings.ts + - server/src/routes/hardware.ts + - server/src/routes/nexus-settings.ts + - server/src/app.ts + - server/src/data/ollama-model-catalog.json + - server/src/services/ollama.ts + - server/src/__tests__/30-hardware-detection.test.ts +autonomous: true +requirements: + - ONBD-02 + - ONBD-03 + - ONBD-01 + +must_haves: + truths: + - "GET /api/system/providers returns 200 with hardware info without any auth token" + - "Apple Silicon is detected via CPU brand string and returns unifiedMemory: true with hardwareTier: apple_silicon" + - "GPU detection via systeminformation has a 3-second timeout; failure degrades to cpu_only tier" + - "nexusSettingsService persists mode to data/nexus-settings.json and reads it back" + - "PATCH /api/nexus/settings requires board auth and persists the mode value" + - "Model catalog contains tier field on every variant and includes qwen3:8b family" + - "getRecommendedModel filters by hardware tier when tier data is present" + artifacts: + - path: "server/src/services/hardware.ts" + provides: "hardwareService with detect() returning HardwareInfo" + exports: ["hardwareService", "HardwareInfo", "HardwareTier"] + - path: "server/src/services/nexus-settings.ts" + provides: "File-backed nexus settings persistence" + exports: ["nexusSettingsService", "NexusMode", "NEXUS_MODES"] + - path: "server/src/routes/hardware.ts" + provides: "Unauthenticated GET /api/system/providers" + exports: ["hardwareRoutes"] + - path: "server/src/routes/nexus-settings.ts" + provides: "Board-auth-gated GET/PATCH /api/nexus/settings" + exports: ["nexusSettingsRoutes"] + - path: "server/src/data/ollama-model-catalog.json" + provides: "Extended model catalog with tier arrays and qwen3 family" + contains: "qwen3" + - path: "server/src/__tests__/30-hardware-detection.test.ts" + provides: "Unit tests for hardware service, settings service, routes, and catalog" + key_links: + - from: "server/src/routes/hardware.ts" + to: "server/src/services/hardware.ts" + via: "hardwareService().detect()" + pattern: "hardwareService.*detect" + - from: "server/src/app.ts" + to: "server/src/routes/hardware.ts" + via: "app.use before api router" + pattern: "hardwareRoutes" + - from: "server/src/services/ollama.ts" + to: "server/src/data/ollama-model-catalog.json" + via: "loadCatalog()" + pattern: "loadCatalog" +--- + + +Build the server-side hardware detection, mode persistence, and model catalog infrastructure for Phase 30. + +Purpose: Provides the unauthenticated hardware probe endpoint, file-backed mode persistence, and tier-aware model catalog that the onboarding UI (Plan 02) will consume. These are the foundational APIs for the entire v1.5 onboarding stack. + +Output: Five new server files (hardware service, hardware route, nexus-settings service, nexus-settings route, tests), two modified files (app.ts mount, ollama-model-catalog.json extension), and one updated file (ollama.ts for tier-aware recommendations). + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/30-hardware-detection-mode-selection/30-RESEARCH.md +@.planning/phases/30-hardware-detection-mode-selection/30-CONTEXT.md + +@server/src/app.ts +@server/src/services/ollama.ts +@server/src/data/ollama-model-catalog.json +@server/src/home-paths.ts +@server/src/routes/ollama.ts + + + + +From server/src/services/ollama.ts: +```typescript +interface CatalogVariant { + name: string; + ramGb: number; + vramGb: number; + quality: string; +} +interface CatalogFamily { + family: string; + variants: CatalogVariant[]; +} +interface ModelCatalog { + models: CatalogFamily[]; +} +export function getRecommendedModel(models: OllamaModel[], systemRamBytes: number): OllamaModel[] +``` + +From server/src/home-paths.ts: +```typescript +export function resolvePaperclipHomeDir(): string; +export function resolvePaperclipInstanceRoot(instanceId?: string): string; +``` + +From server/src/middleware/auth.ts: +```typescript +// req.actor.type === "board" | "agent" | "none" +// assertBoard(req) throws 403 if not board +``` + +From server/src/app.ts (mounting pattern — line ~129): +```typescript +app.use(llmRoutes(db)); // mounted before api router +// ... +const api = Router(); +api.use(boardMutationGuard()); +// ... all authenticated routes on api ... +app.use("/api", api); +``` + + + + + + + Task 1: Hardware service, nexus-settings service, model catalog, and tests + + server/src/services/hardware.ts + server/src/services/nexus-settings.ts + server/src/data/ollama-model-catalog.json + server/src/services/ollama.ts + server/src/__tests__/30-hardware-detection.test.ts + + + server/src/services/ollama.ts + server/src/data/ollama-model-catalog.json + server/src/home-paths.ts + server/src/services/instance-settings.ts + + + - Test: hardwareService().detect() returns HardwareInfo with all required fields (totalGb, freeGb, usableGb, platform, gpuName, gpuVramGb, unifiedMemory, hardwareTier, cpuModel) + - Test: When os.cpus()[0].model starts with "Apple" and platform is "darwin", returns unifiedMemory: true, hardwareTier: "apple_silicon", gpuVramGb: null + - Test: When si.graphics() returns a controller with vram >= 4096 MB, returns hardwareTier: "gpu" with gpuVramGb set + - Test: When si.graphics() returns no controllers (or throws), returns hardwareTier: "cpu_only" + - Test: si.graphics() is wrapped in Promise.race with 3000ms timeout; if it times out, returns cpu_only tier + - Test: nexusSettingsService().get() returns { mode: "both" } when no file exists (default) + - Test: nexusSettingsService().set({ mode: "personal_ai" }) writes to disk and subsequent get() returns "personal_ai" + - Test: nexusSettingsService().set({ mode: "invalid" as any }) throws Zod validation error + - Test: Extended catalog JSON contains a "qwen3" family with variant "qwen3:8b" having tier array ["gpu", "apple_silicon", "cpu_only"] + - Test: Every variant in catalog has a "tier" array (no variant without tier) + - Test: getRecommendedModel with tier "gpu" only recommends models whose tier includes "gpu" + + + **1. Create `server/src/services/hardware.ts`:** + + Export types `HardwareTier = "gpu" | "apple_silicon" | "cpu_only"` and `HardwareInfo` interface with fields: `totalGb: number`, `freeGb: number`, `usableGb: number`, `platform: NodeJS.Platform`, `gpuName: string | null`, `gpuVramGb: number | null`, `unifiedMemory: boolean`, `hardwareTier: HardwareTier`, `cpuModel: string | null`. + + Export `hardwareService()` factory function returning `{ detect }`. Implementation: + - Get `totalBytes = os.totalmem()`, `freeBytes = os.freemem()`, compute `totalGb`, `freeGb`, `usableGb = freeGb * 0.75` (all rounded to 1 decimal). + - Get `cpuModel = os.cpus()[0]?.model ?? null`. + - Detect Apple Silicon: `process.platform === "darwin" && cpuModel?.startsWith("Apple")`. + - If Apple Silicon: set `gpuName: null`, `gpuVramGb: null`, `unifiedMemory: true`, `hardwareTier: "apple_silicon"`. Do NOT call si.graphics(). + - If not Apple Silicon: call `si.graphics()` wrapped in `Promise.race()` with a 3000ms timeout. On success, read `controllers[0].model` for `gpuName` and `controllers[0].vram / 1024` for `gpuVramGb`. If `gpuVramGb >= 4`, set `hardwareTier: "gpu"`. Otherwise `"cpu_only"`. On failure/timeout, set `gpuName: null`, `gpuVramGb: null`, `hardwareTier: "cpu_only"`. + - Cache result for 5 minutes (same pattern as in RESEARCH.md: `cache` variable + `cacheExpiry` timestamp). + - Import: `import os from "node:os"; import si from "systeminformation";` + + **2. Create `server/src/services/nexus-settings.ts`:** + + Export `NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const`, `NexusMode` type, and `nexusSettingsService()` factory. + + Use Zod schema: `z.object({ mode: z.enum(NEXUS_MODES).default("both") })`. + + `resolveNexusSettingsPath()`: `path.resolve(resolvePaperclipInstanceRoot(), "data", "nexus-settings.json")`. + + Methods: + - `get()`: Read file, parse with Zod. On any error (file missing, invalid JSON), return `{ mode: "both" }`. + - `set(patch)`: Load current, merge patch, validate with Zod, write JSON to disk (mkdirSync recursive for data dir). + + Import `resolvePaperclipInstanceRoot` from `"../home-paths.js"`. + + **3. Extend `server/src/data/ollama-model-catalog.json`:** + + Add `"tier"` array to every existing variant. Add two new families: + + ```json + { + "family": "qwen3", + "variants": [ + { "name": "qwen3:8b", "ramGb": 5, "vramGb": 5, "quality": "balanced", "tier": ["gpu", "apple_silicon", "cpu_only"] } + ] + } + ``` + + Tier assignments for existing variants: + - qwen2.5-coder:7b → ["gpu", "apple_silicon", "cpu_only"] + - qwen2.5-coder:14b → ["gpu", "apple_silicon"] + - qwen2.5-coder:32b → ["gpu"] + - llama3.2:3b → ["gpu", "apple_silicon", "cpu_only"] + - llama3.1:8b → ["gpu", "apple_silicon", "cpu_only"] + - llama3.1:70b → ["gpu"] + - mistral:7b → ["gpu", "apple_silicon", "cpu_only"] + - mistral:22b → ["gpu", "apple_silicon"] + - phi4:14b → ["gpu", "apple_silicon"] + - deepseek-r1:7b → ["gpu", "apple_silicon", "cpu_only"] + - deepseek-r1:32b → ["gpu", "apple_silicon"] + + **4. Update `server/src/services/ollama.ts`:** + + Update `CatalogVariant` interface: add optional `tier?: string[]` field. + + Update `getRecommendedModel` signature to accept an optional third parameter `hardwareTier?: HardwareTier`: + ```typescript + export function getRecommendedModel( + models: OllamaModel[], + systemRamBytes: number, + hardwareTier?: "gpu" | "apple_silicon" | "cpu_only", + ): OllamaModel[] + ``` + + In the loop that finds `bestEntry`, add a tier filter: if `hardwareTier` is provided AND `entry.tier` exists AND `!entry.tier.includes(hardwareTier)`, skip that entry. Existing behavior (no hardwareTier passed) is unchanged. + + **5. Create `server/src/__tests__/30-hardware-detection.test.ts`:** + + Use Vitest. Mock `os` and `systeminformation` with `vi.mock()`. + + Test groups: + - `describe("hardwareService")` — test detect() for Apple Silicon, GPU, CPU-only, and timeout scenarios + - `describe("nexusSettingsService")` — test default, set/get, and validation error (use a temp dir via `vi.mock` of home-paths or `os.tmpdir()`) + - `describe("model catalog")` — load the JSON file, verify every variant has `tier` array, verify qwen3:8b exists + - `describe("getRecommendedModel with tier")` — test that tier filtering works correctly + + Install systeminformation: the executor must run `pnpm --filter server add systeminformation@5` before creating hardware.ts. + + + cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection + + + - server/src/services/hardware.ts exports `hardwareService`, `HardwareInfo`, `HardwareTier` + - server/src/services/hardware.ts contains `Promise.race` with `3000` timeout for si.graphics + - server/src/services/hardware.ts contains `cpuModel?.startsWith("Apple")` + - server/src/services/hardware.ts contains `usableGb = freeGb * 0.75` (or equivalent `freeBytes * 0.75`) + - server/src/services/nexus-settings.ts exports `nexusSettingsService`, `NexusMode`, `NEXUS_MODES` + - server/src/services/nexus-settings.ts contains `z.enum(NEXUS_MODES).default("both")` + - server/src/services/nexus-settings.ts contains `resolvePaperclipInstanceRoot` + - server/src/data/ollama-model-catalog.json contains `"qwen3"` family + - server/src/data/ollama-model-catalog.json every variant object contains `"tier"` key + - server/src/services/ollama.ts CatalogVariant interface contains `tier` + - server/src/services/ollama.ts getRecommendedModel accepts `hardwareTier` parameter + - server/src/__tests__/30-hardware-detection.test.ts exists and exits 0 + + Hardware detection service returns correct tier for Apple Silicon, GPU, and CPU-only. Nexus settings service persists mode to disk. Model catalog has tier arrays on every variant. getRecommendedModel filters by hardware tier. All tests pass. + + + + Task 2: Hardware and nexus-settings routes, app.ts mounting + + server/src/routes/hardware.ts + server/src/routes/nexus-settings.ts + server/src/app.ts + + + server/src/app.ts + server/src/routes/ollama.ts + server/src/routes/instance-settings.ts + server/src/middleware/auth.ts + server/src/services/hardware.ts + server/src/services/nexus-settings.ts + + + **1. Create `server/src/routes/hardware.ts`:** + + Export `hardwareRoutes()` function returning an Express Router. + + Single route: `router.get("/system/providers", async (_req, res) => { ... })`. + + Call `hardwareService().detect()`. On success, return `res.json(info)`. On error, return a graceful degradation JSON with `os.totalmem()`, `os.freemem()`, `platform`, all GPU fields null, `hardwareTier: "cpu_only"` (exact shape from RESEARCH.md Pattern 1 fallback). + + This route is intentionally unauthenticated. Add a code comment: `// Unauthenticated — hardware is a property of the machine, not the user. Safe: read-only, no mutation, no secrets.` + + Also add a `GET /system/providers/recommendation` route that: + - Calls `hardwareService().detect()` to get the hardware info + - Calls `loadCatalog()` from ollama service (or reads the catalog directly) to get model families + - Returns `{ hardwareInfo, recommendedModels }` where `recommendedModels` is a filtered list of catalog entries matching the detected hardware tier + - This gives the UI a single endpoint to show "what model do we recommend for your hardware" without needing Ollama installed + + Import: `import os from "node:os"`, `import { hardwareService } from "../services/hardware.js"`. + + **2. Create `server/src/routes/nexus-settings.ts`:** + + Export `nexusSettingsRoutes()` function returning an Express Router. + + Two routes: + - `GET /nexus/settings` — calls `nexusSettingsService().get()`, returns JSON. Guard with `assertBoard(req)`. + - `PATCH /nexus/settings` — reads `req.body`, calls `nexusSettingsService().set(req.body)`, returns updated settings. Guard with `assertBoard(req)`. + + Import `assertBoard` from `"./authz.js"` (same pattern as `instanceSettingsRoutes`). + + **3. Modify `server/src/app.ts`:** + + Add import at top: + ```typescript + import { hardwareRoutes } from "./routes/hardware.js"; + import { nexusSettingsRoutes } from "./routes/nexus-settings.js"; + ``` + + Mount hardware routes BEFORE the `const api = Router()` block — specifically right after `app.use(llmRoutes(db));` (line ~129 in current file). This places it after actorMiddleware runs but the route itself does not call assertBoard: + ```typescript + app.use("/api", hardwareRoutes()); + ``` + + CRITICAL: The hardware route must come BEFORE `app.use("/api", api)` so it is reached without boardMutationGuard. The llmRoutes mount point (line ~129) is the correct insertion location — right after it. + + Mount nexus settings routes on the `api` Router (authenticated): + ```typescript + api.use(nexusSettingsRoutes()); + ``` + + Place this after `api.use(instanceSettingsRoutes(db));` for logical grouping. + + + cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection + + + - server/src/routes/hardware.ts exports `hardwareRoutes` + - server/src/routes/hardware.ts contains `router.get("/system/providers"` + - server/src/routes/hardware.ts contains comment with "Unauthenticated" + - server/src/routes/nexus-settings.ts exports `nexusSettingsRoutes` + - server/src/routes/nexus-settings.ts contains `assertBoard` + - server/src/routes/nexus-settings.ts contains `router.get("/nexus/settings"` + - server/src/routes/nexus-settings.ts contains `router.patch("/nexus/settings"` + - server/src/app.ts contains `import { hardwareRoutes }` from `"./routes/hardware.js"` + - server/src/app.ts contains `import { nexusSettingsRoutes }` from `"./routes/nexus-settings.js"` + - server/src/app.ts contains `app.use("/api", hardwareRoutes())` BEFORE the `const api = Router()` line + - server/src/app.ts contains `api.use(nexusSettingsRoutes())` + + Hardware probe endpoint returns 200 without auth. Nexus settings endpoints require board auth. Both are correctly mounted in app.ts. All existing tests still pass. + + + + + +Run the full server test suite to ensure no regressions: +```bash +cd /opt/nexus && pnpm --filter server test --run +``` + +Verify the hardware probe is unauthenticated by checking that `hardwareRoutes` is mounted before `boardMutationGuard`: +```bash +grep -n "hardwareRoutes\|const api = Router\|boardMutationGuard" server/src/app.ts +``` + +Verify the model catalog has tier on every variant: +```bash +node -e "const c = require('./server/src/data/ollama-model-catalog.json'); const all = c.models.flatMap(f => f.variants); const missing = all.filter(v => !v.tier); console.log(missing.length === 0 ? 'OK: all variants have tier' : 'FAIL: ' + missing.length + ' variants missing tier')" +``` + + + +1. `pnpm --filter server test --run -- 30-hardware-detection` exits 0 +2. `pnpm --filter server test --run` exits 0 (no regressions) +3. `server/src/services/hardware.ts` exists with hardwareService, HardwareInfo, HardwareTier exports +4. `server/src/services/nexus-settings.ts` exists with nexusSettingsService, NexusMode exports +5. `server/src/routes/hardware.ts` exists with unauthenticated GET /system/providers +6. `server/src/routes/nexus-settings.ts` exists with board-auth-gated GET/PATCH +7. Model catalog has tier arrays and qwen3 family +8. getRecommendedModel supports optional hardwareTier parameter + + + +After completion, create `.planning/phases/30-hardware-detection-mode-selection/30-01-SUMMARY.md` + diff --git a/.planning/phases/30-hardware-detection-mode-selection/30-02-PLAN.md b/.planning/phases/30-hardware-detection-mode-selection/30-02-PLAN.md new file mode 100644 index 00000000..4c5cf7eb --- /dev/null +++ b/.planning/phases/30-hardware-detection-mode-selection/30-02-PLAN.md @@ -0,0 +1,463 @@ +--- +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" +--- + + +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). + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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: +```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 } +}; +``` + + + + + + + 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` — `api.get("/system/providers")` + - `fetchNexusSettings(): Promise` — `api.get("/nexus/settings")` + - `updateNexusSettings(settings: Partial): Promise` — `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({ + 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 `

Could not detect hardware. You can still continue.

`. 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: `

Slower than GPU-accelerated models -- cloud AI recommended

` + + Each stat row: `
{label}{value}
` + + b) Privacy frame (shown when `hardwareTier !== "cpu_only"`): + ```tsx +
+ Local AI (recommended for privacy) + + Runs entirely on your machine.{"\n"} + No accounts. No tracking. Works offline. + +
+ ``` + + 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("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 +

Step {step} of 3

+ ``` + + **Step 1 — Hardware Detection:** + - Heading: `hwLoading ? "Detecting your hardware..." : "Your hardware"` (text-2xl font-semibold, per UI-SPEC) + - Body: `` + - 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: `` + - 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: +```bash +cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection +``` + +Dev server runs without errors: +```bash +cd /opt/nexus && pnpm dev +``` + + + +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 + + + +After completion, create `.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md` +