docs(30): create phase plan
This commit is contained in:
parent
c287d971e4
commit
010014187e
3 changed files with 851 additions and 2 deletions
|
|
@ -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 | - |
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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).
|
||||
</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-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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
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);
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Hardware service, nexus-settings service, model catalog, and tests</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
server/src/services/ollama.ts
|
||||
server/src/data/ollama-model-catalog.json
|
||||
server/src/home-paths.ts
|
||||
server/src/services/instance-settings.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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"
|
||||
</behavior>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Hardware and nexus-settings routes, app.ts mounting</name>
|
||||
<files>
|
||||
server/src/routes/hardware.ts
|
||||
server/src/routes/nexus-settings.ts
|
||||
server/src/app.ts
|
||||
</files>
|
||||
<read_first>
|
||||
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
|
||||
</read_first>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter server test --run -- 30-hardware-detection</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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())`
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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')"
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/30-hardware-detection-mode-selection/30-01-SUMMARY.md`
|
||||
</output>
|
||||
|
|
@ -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"
|
||||
---
|
||||
|
||||
<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>
|
||||
Loading…
Add table
Reference in a new issue