// [nexus] Replacement onboarding wizard — 3-step flow: hardware detection, mode selection, root directory // 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 { 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 { useHardwareInfo } from "../hooks/useHardwareInfo"; import { updateNexusSettings, type NexusMode } from "../api/hardware"; // [nexus] 3-step onboarding wizard: hardware detection → mode selection → root directory 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); // 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 = root directory const [step, setStep] = useState(1); // Mode state: "both" pre-selected per UI-SPEC const [selectedMode, setSelectedMode] = useState("both"); // 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 Hermes when wizard opens const [defaultAdapter, setDefaultAdapter] = useState<"claude_local" | "hermes_local">("claude_local"); const [probing, setProbing] = useState(false); // Reset form when wizard closes useEffect(() => { if (!effectiveOnboardingOpen) { setRootDir(""); setError(null); setLoading(false); setStep(1); setSelectedMode("both"); } }, [effectiveOnboardingOpen]); // [nexus] Probe for Hermes availability when wizard opens useEffect(() => { if (!effectiveOnboardingOpen) return; setProbing(true); agentsApi.probeAdapter("hermes_local") .then((data) => { if (data.available) setDefaultAdapter("hermes_local"); }) .catch(() => {}) // graceful — keep claude_local .finally(() => setProbing(false)); }, [effectiveOnboardingOpen]); function handleClose() { setRouteDismissed(true); closeOnboarding(); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (defaultAdapter === "claude_local" && !rootDir.trim()) return; setLoading(true); setError(null); try { // 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 // (display label is "Project Manager" via AGENT_ROLE_LABELS; ceo/ bundle // has been rewritten with PM content in 04-01) 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 } // 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); } } if (!effectiveOnboardingOpen) return null; return (
{/* Backdrop */}
{/* Card */}
{/* Step indicator */}

Step {step} of 3

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

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

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

Choose your mode

)} {/* Step 3 — Root Directory */} {step === 3 && ( <> {/* 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}

)}
)}
); }