15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32-multi-step-onboarding-wizard | 01 | execute | 1 |
|
true |
|
|
Purpose: Users can skip through onboarding quickly or review their choices before starting — fulfilling ONBD-04 (skip any step), ONBD-05 (summary screen), and ONBD-06 (one-click to chat).
Output: OnboardingSummaryStep component, updated NexusOnboardingWizard with 5 steps and skip buttons, unit tests.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/32-multi-step-onboarding-wizard/32-RESEARCH.mdFrom ui/src/api/hardware.ts:
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";
From ui/src/context/ChatPanelContext.tsx:
export function useChatPanel(): {
chatOpen: boolean;
setChatOpen: (open: boolean) => void;
};
From ui/src/components/NexusOnboardingWizard.tsx (existing state available to summary):
// These are the wizard state variables available to pass as props:
const [selectedMode, setSelectedMode] = useState<NexusMode>("both");
const [puterToken, setPuterToken] = useState<string | null>(null);
const [googleOAuthStateId, setGoogleOAuthStateId] = useState<string | null>(null);
const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null);
const [rootDir, setRootDir] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: hardwareInfo } = useHardwareInfo(effectiveOnboardingOpen);
Task 1: Create OnboardingSummaryStep component and tests
ui/src/components/onboarding/OnboardingSummaryStep.tsx, ui/src/components/onboarding/OnboardingSummaryStep.test.tsx
- ui/src/components/onboarding/HardwareSummaryStep.tsx (existing step pattern, styling conventions)
- ui/src/components/onboarding/ModeSelector.tsx (NexusMode usage, label patterns)
- ui/src/api/hardware.ts (HardwareInfo and NexusMode types)
Create `ui/src/components/onboarding/OnboardingSummaryStep.tsx`:
- Define props interface:
interface OnboardingSummaryStepProps {
hardwareInfo: HardwareInfo | undefined;
selectedMode: NexusMode;
providerLabel: string;
rootDir: string;
loading: boolean;
error: string | null;
onStartChat: () => void;
onBack: () => void;
}
-
Implement a read-only summary with 4 rows in a bordered card (
rounded-lg border border-border p-4):- Hardware: Show
hardwareInfo.hardwareTiermapped to display labels ("GPU", "Apple Silicon", "CPU Only"), or "Unknown" if undefined - Mode: Map
NexusModeto labels:personal_ai-> "Personal AI Assistant",project_builder-> "Project Builder",both-> "Both (recommended)" - Provider: Render the
providerLabelstring directly (caller computes it) - Root directory: Only show row if
rootDiris non-empty, render value infont-mono text-sm
- Hardware: Show
-
Create a helper
SummaryRowcomponent (internal, not exported) that renders a label-value pair:- Label in
text-muted-foregroundon the left - Value on the right, optionally mono (
font-mono text-smwhenmonoprop is true) - Use flexbox
justify-between
- Label in
-
Show error message if
erroris non-null (text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2) -
"Start chatting" primary button —
onClick={onStartChat},disabled={loading}, shows "Setting up..." when loading (with the same spinner SVG used in the root dir step) -
"Back" ghost button —
onClick={onBack},disabled={loading} -
Do NOT include any corporate language ("company", "CEO", "mission") per ROADMAP success criteria.
Create ui/src/components/onboarding/OnboardingSummaryStep.test.tsx:
Using Vitest + React Testing Library (project standard):
- Test 1: Renders all summary rows with provided data (hardware tier, mode label, provider label, root dir)
- Test 2: Hides root directory row when rootDir is empty string
- Test 3: Shows error message when error prop is non-null
- Test 4: Calls onStartChat when "Start chatting" button clicked
- Test 5: Disables "Start chatting" button when loading is true
- Test 6: Shows "Setting up..." text when loading is true
Mock data: hardwareInfo with hardwareTier: "apple_silicon", selectedMode: "both", providerLabel: "Puter (free, zero-config)", rootDir: "~/projects/test".
cd /opt/nexus && pnpm vitest run ui/src/components/onboarding/OnboardingSummaryStep.test.tsx
<acceptance_criteria>
- grep -q "export function OnboardingSummaryStep" ui/src/components/onboarding/OnboardingSummaryStep.tsx
- grep -q "Start chatting" ui/src/components/onboarding/OnboardingSummaryStep.tsx
- grep -q "onStartChat" ui/src/components/onboarding/OnboardingSummaryStep.tsx
- grep -q "SummaryRow" ui/src/components/onboarding/OnboardingSummaryStep.tsx
- grep -q "describe" ui/src/components/onboarding/OnboardingSummaryStep.test.tsx
</acceptance_criteria>
OnboardingSummaryStep renders hardware, mode, provider, root dir in a read-only card with Start chatting CTA. All 6 tests pass.
1. Import additions (top of file):
import { OnboardingSummaryStep } from "./onboarding/OnboardingSummaryStep";import { useChatPanel } from "../context/ChatPanelContext";
2. Add useChatPanel hook inside the OnboardingWizard component body, near the other hooks:
const { setChatOpen } = useChatPanel();
3. Add deriveProviderLabel helper inside or before the component:
function deriveProviderLabel(
puterToken: string | null,
googleOAuthStateId: string | null,
apiKeyData: { provider: string; apiKey: string } | null,
): string {
if (puterToken) return "Puter (free, zero-config)";
if (googleOAuthStateId) return "Google Gemini (free tier)";
if (apiKeyData) return `API key — ${apiKeyData.provider}`;
return "None selected";
}
4. Update step indicator — change <p className="text-xs text-muted-foreground text-center">Step {step} of 4</p> to:
<p className="text-xs text-muted-foreground text-center">
{step === 5 ? "Summary" : `Step ${step} of 4`}
</p>
Note: Keep "of 4" — the summary is not a form step, so steps 1-4 are the data entry steps. Step 5 shows "Summary" label instead.
5. Add skip button to Step 1 (hardware detection) — after the existing Continue button, add:
<Button type="button" variant="ghost" onClick={() => setStep(2)} className="w-full">
Skip
</Button>
6. Add skip button to Step 2 (mode selection) — after the Back button in the button group, add:
<Button type="button" variant="ghost" onClick={() => setStep(3)} className="w-full">
Skip
</Button>
7. Modify Step 4 "Get Started" button — change from type="submit" to type="button" and change its onClick to advance to step 5 instead of submitting:
<Button
type="button"
onClick={() => setStep(5)}
disabled={loading || probing}
className="w-full"
>
Review & finish
</Button>
Also remove the <form onSubmit={handleSubmit}> wrapper around step 4 — replace with a plain <div className="flex flex-col gap-4">. The submit now happens on step 5.
Also add skip button to Step 4 after the Back button:
<Button type="button" variant="ghost" onClick={() => setStep(5)} className="w-full" disabled={loading}>
Skip to summary
</Button>
8. Add Step 5 — Summary after the step 4 block:
{step === 5 && (
<OnboardingSummaryStep
hardwareInfo={hardwareInfo}
selectedMode={selectedMode}
providerLabel={deriveProviderLabel(puterToken, googleOAuthStateId, apiKeyData)}
rootDir={rootDir}
loading={loading}
error={error}
onStartChat={handleStartChat}
onBack={() => setStep(4)}
/>
)}
9. Create handleStartChat function — adapts existing handleSubmit logic but also opens chat:
async function handleStartChat() {
// Guard: claude_local requires rootDir
if (defaultAdapter === "claude_local" && !rootDir.trim()) {
setError("Root directory is required for Claude Code. Go back to step 4 to set it.");
return;
}
setLoading(true);
setError(null);
try {
// === Same company creation logic as handleSubmit ===
const company = await companiesApi.create({ name: VOCAB.appName });
setSelectedCompanyId(company.id);
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
// ... (copy all the agent creation, mode save, and credential storage code
// from handleSubmit — the baseAdapterConfig, hermesPromptTemplate,
// adapterConfig, runtimeConfig, PM agent create, Engineer agent create,
// queryClient invalidation, updateNexusSettings, puterToken/google/apiKey storage)
// Navigate to dashboard then open chat panel
closeOnboarding();
navigate(`/${company.issuePrefix}/dashboard`);
setChatOpen(true); // <-- ONBD-06: opens chat panel after navigation
} catch (err) {
setError(err instanceof Error ? err.message : "Setup failed. Please try again.");
setLoading(false);
}
}
IMPORTANT: The handleStartChat function contains the SAME company/agent creation logic as the existing handleSubmit. The only differences are: (a) no e: React.FormEvent parameter, (b) error message mentions going back to step 4 for rootDir, (c) setChatOpen(true) is called after navigate(). You can either refactor the shared logic into an internal helper or duplicate it — refactoring into a helper like createWorkspace() is cleaner.
10. Update the reset effect — in the useEffect that resets form state when wizard closes, change setStep(1) to still reset to 1 (no change needed — step 5 naturally resets).
11. Update the top comment — change "4-step flow" to "5-step flow" and update the step list to include "summary". cd /opt/nexus && pnpm vitest run ui/src/components/onboarding/ <acceptance_criteria> - grep -q "OnboardingSummaryStep" ui/src/components/NexusOnboardingWizard.tsx - grep -q "useChatPanel" ui/src/components/NexusOnboardingWizard.tsx - grep -q "setChatOpen(true)" ui/src/components/NexusOnboardingWizard.tsx - grep -q "step === 5" ui/src/components/NexusOnboardingWizard.tsx - grep -q "deriveProviderLabel" ui/src/components/NexusOnboardingWizard.tsx - grep -c "Skip" ui/src/components/NexusOnboardingWizard.tsx shows at least 3 skip buttons - grep -q "Summary" ui/src/components/NexusOnboardingWizard.tsx (step indicator label) - grep -q "handleStartChat" ui/src/components/NexusOnboardingWizard.tsx </acceptance_criteria> Wizard has 5 steps with skip on 1/2/4, summary on step 5, and "Start chatting" creates workspace then opens chat panel. TypeScript compiles and tests pass.
1. `pnpm vitest run ui/src/components/onboarding/` — all unit tests pass 2. `cd /opt/nexus && pnpm tsc --noEmit` — no TypeScript errors 3. Grep checks: `grep -q "setChatOpen(true)" ui/src/components/NexusOnboardingWizard.tsx` confirms ONBD-06 wiring 4. Grep checks: `grep -c "Skip" ui/src/components/NexusOnboardingWizard.tsx` confirms at least 3 skip buttons (ONBD-04) 5. Grep checks: `grep -q "OnboardingSummaryStep" ui/src/components/NexusOnboardingWizard.tsx` confirms summary wired (ONBD-05)<success_criteria>
- OnboardingSummaryStep component exists and renders hardware, mode, provider, root dir
- Skip buttons on steps 1, 2, and 4 (step 3 already has one)
- Step 5 shows "Summary" in the step indicator
- "Start chatting" on summary creates workspace, navigates to dashboard, and opens chat panel
- No corporate language ("company", "CEO", "mission") in the summary step
- All unit tests pass
- TypeScript compiles cleanly </success_criteria>