// [nexus] Replacement onboarding wizard — 5-step flow: hardware detection, mode selection, provider selection, root directory, summary // Exports `OnboardingWizard` to match the named import in App.tsx. // Wired via Vite alias: all imports of ./components/OnboardingWizard are // redirected here at build time; the original file is preserved for upstream rebase. import { useState, useEffect } from "react"; import { useLocation, useNavigate, useParams } from "@/lib/router"; import { VOCAB } from "@paperclipai/branding"; import { useQueryClient } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { companiesApi } from "../api/companies"; import { agentsApi } from "../api/agents"; import { puterProxyApi } from "../api/puter-proxy"; import { queryKeys } from "../lib/queryKeys"; import { resolveRouteOnboardingOptions } from "../lib/onboarding-route"; import { Dialog, DialogPortal } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "../lib/utils"; import { ModeSelector } from "./onboarding/ModeSelector"; import { HardwareSummaryStep } from "./onboarding/HardwareSummaryStep"; import { ProviderSelectionStep } from "./onboarding/ProviderSelectionStep"; import { OnboardingSummaryStep } from "./onboarding/OnboardingSummaryStep"; import { useHardwareInfo } from "../hooks/useHardwareInfo"; import { updateNexusSettings, type NexusMode } from "../api/hardware"; import { useChatPanel } from "../context/ChatPanelContext"; 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"; } // [nexus] 5-step onboarding wizard: hardware detection → mode selection → provider selection → root directory → summary export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany(); const queryClient = useQueryClient(); const navigate = useNavigate(); const location = useLocation(); const { companyPrefix } = useParams<{ companyPrefix?: string }>(); const [routeDismissed, setRouteDismissed] = useState(false); const { setChatOpen } = useChatPanel(); // Preserve wizard-show detection logic from the original OnboardingWizard const routeOnboardingOptions = companyPrefix && companiesLoading ? null : resolveRouteOnboardingOptions({ pathname: location.pathname, companyPrefix, companies, }); const effectiveOnboardingOpen = onboardingOpen || (routeOnboardingOptions !== null && !routeDismissed); useEffect(() => { setRouteDismissed(false); }, [location.pathname]); // Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = root directory, 5 = summary const [step, setStep] = useState(1); // Mode state: "both" pre-selected per UI-SPEC const [selectedMode, setSelectedMode] = useState("both"); // [nexus] Provider credentials — captured in React state for post-company-creation submission const [puterToken, setPuterToken] = useState(null); const [googleOAuthStateId, setGoogleOAuthStateId] = useState(null); const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null); // Form state const [rootDir, setRootDir] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // [nexus] Hardware detection — only fetch when wizard is open const { data: hardwareInfo, isLoading: hwLoading, isError: hwError } = useHardwareInfo(effectiveOnboardingOpen); // [nexus] Adapter detection state — probe for adapters when wizard opens const [defaultAdapter, setDefaultAdapter] = useState<"claude_local" | "hermes_local">("claude_local"); const [probing, setProbing] = useState(false); const [detectedAdapters, setDetectedAdapters] = useState>({}); // Reset form when wizard closes useEffect(() => { if (!effectiveOnboardingOpen) { setRootDir(""); setError(null); setLoading(false); setStep(1); setSelectedMode("both"); setPuterToken(null); setGoogleOAuthStateId(null); setApiKeyData(null); } }, [effectiveOnboardingOpen]); // [nexus] Probe for adapter availability when wizard opens (parallel, fire-and-forget) useEffect(() => { if (!effectiveOnboardingOpen) return; setProbing(true); const probes = Promise.all([ agentsApi.probeAdapter("hermes_local").catch(() => ({ available: false, status: "error" })), agentsApi.probeAdapter("claude_local").catch(() => ({ available: false, status: "error" })), agentsApi.probeAdapter("openclaw_gateway").catch(() => ({ available: false, status: "error" })), ]); probes.then(([hermes, claude, openclaw]) => { const detected: Record = { hermes_local: hermes.available, claude_local: claude.available, openclaw_gateway: openclaw.available, }; setDetectedAdapters(detected); if (hermes.available) setDefaultAdapter("hermes_local"); }).catch(() => {}).finally(() => setProbing(false)); }, [effectiveOnboardingOpen]); function handleClose() { setRouteDismissed(true); closeOnboarding(); } // [nexus] Shared workspace creation logic used by both handleSubmit (step 4 direct) and handleStartChat (step 5) async function createWorkspace() { // Step 1: Create workspace (company) named after VOCAB.appName const company = await companiesApi.create({ name: VOCAB.appName }); setSelectedCompanyId(company.id); queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); // [nexus] hermes_local doesn't require a cwd; directory is optional const baseAdapterConfig = defaultAdapter === "hermes_local" ? (rootDir.trim() ? { cwd: rootDir.trim() } : {}) : { cwd: rootDir.trim() }; // [nexus] Hermes agents need a promptTemplate so they follow the Nexus heartbeat // workflow. The server's ensureDefaultInstructionsBundle will materialize this as // AGENTS.md alongside the full role bundle (HEARTBEAT.md, SOUL.md, TOOLS.md). const hermesPromptTemplate = [ `You are "{{agentName}}", an AI agent managed by ${VOCAB.appName}.`, "", "Your identity:", " Agent ID: {{agentId}}", " Company ID: {{companyId}}", " API Base: {{paperclipApiUrl}}", " Run ID: {{runId}}", "", "IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.", 'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.', "", "Before starting any task:", "1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions", "2. Follow the HEARTBEAT.md workflow from your instructions", "3. Use TOOLS.md for available API endpoints", "", "{{#taskId}}", "Assigned task: {{taskId}} - {{taskTitle}}", "{{/taskId}}", ].join("\n"); const adapterConfig = defaultAdapter === "hermes_local" ? { ...baseAdapterConfig, promptTemplate: hermesPromptTemplate, persistSession: true } : baseAdapterConfig; const runtimeConfig = { heartbeat: { enabled: true, intervalSec: 3600, wakeOnDemand: true, cooldownSec: 10, maxConcurrentRuns: 1, }, }; // Step 2: Create PM agent with role "ceo" for elevated permissions await agentsApi.create(company.id, { name: "Project Manager", role: "ceo", adapterType: defaultAdapter, adapterConfig, runtimeConfig, }); // Step 3: Create Engineer agent await agentsApi.create(company.id, { name: "Engineer", role: "engineer", adapterType: defaultAdapter, adapterConfig, runtimeConfig, }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(company.id), }); // Persist selected mode — non-blocking try { await updateNexusSettings({ mode: selectedMode }); } catch { // Non-blocking — mode defaults to "both" if save fails } // [nexus] Store collected provider credentials after company creation — all non-blocking if (puterToken) { await puterProxyApi.storeToken(company.id, puterToken).catch(() => {}); } if (googleOAuthStateId) { // Claim Google OAuth tokens for this company await puterProxyApi.claimGoogleTokens(googleOAuthStateId, company.id).catch(() => {}); } if (apiKeyData) { await puterProxyApi.storeApiKey(company.id, apiKeyData.provider, apiKeyData.apiKey).catch(() => {}); } return company; } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (defaultAdapter === "claude_local" && !rootDir.trim()) return; setLoading(true); setError(null); try { const company = await createWorkspace(); // Navigate to dashboard — not an issue detail page closeOnboarding(); navigate(`/${company.issuePrefix}/dashboard`); } catch (err) { setError(err instanceof Error ? err.message : "Setup failed. Please try again."); setLoading(false); } } // [nexus] ONBD-06: Creates workspace then opens chat panel after navigation 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 { const company = await createWorkspace(); // Navigate to dashboard then open chat panel closeOnboarding(); navigate(`/${company.issuePrefix}/dashboard`); setChatOpen(true); } catch (err) { setError(err instanceof Error ? err.message : "Setup failed. Please try again."); setLoading(false); } } if (!effectiveOnboardingOpen) return null; return (
{/* Backdrop */}
{/* Card */}
{/* Step indicator */}

{step === 5 ? "Summary" : `Step ${step} of 4`}

{/* Step 1 — Hardware Detection */} {step === 1 && ( <>

{hwLoading ? "Detecting your hardware..." : "Your hardware"}

)} {/* Step 2 — Mode Selection */} {step === 2 && ( <>

Choose your mode

)} {/* Step 3 — Provider Selection (NEW) */} {step === 3 && ( <>

Choose a provider

No API keys needed for the zero-config path.

setApiKeyData({ provider, apiKey })} onSkip={() => setStep(4)} onContinue={() => setStep(4)} detectedAdapters={detectedAdapters} /> )} {/* Step 4 — Root Directory (was step 3) */} {step === 4 && ( <> {/* Header */}

Welcome to {VOCAB.appName}

{defaultAdapter === "hermes_local" ? `${VOCAB.appName} will set up a local AI workspace with a ${VOCAB.ceo.toLowerCase()}, engineer, and generalist — no API key needed.` : `Choose a project root directory. ${VOCAB.appName} will set up a ${VOCAB.ceo.toLowerCase()} and engineer to start working.`}

{/* Form */}
setRootDir(e.target.value)} disabled={loading} autoFocus autoComplete="off" className="font-mono text-sm" />
{error && (

{error}

)}
)} {/* Step 5 — Summary */} {step === 5 && ( setStep(4)} /> )}
); }