diff --git a/ui/src/components/NexusOnboardingWizard.tsx b/ui/src/components/NexusOnboardingWizard.tsx new file mode 100644 index 00000000..8cc23f1f --- /dev/null +++ b/ui/src/components/NexusOnboardingWizard.tsx @@ -0,0 +1,219 @@ +// [nexus] Replacement onboarding wizard — single-step root directory flow +// 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"; + +// [nexus] Single-step onboarding wizard: root directory → workspace + PM + Engineer +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]); + + // Form state + const [rootDir, setRootDir] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Reset form when wizard closes + useEffect(() => { + if (!effectiveOnboardingOpen) { + setRootDir(""); + setError(null); + setLoading(false); + } + }, [effectiveOnboardingOpen]); + + function handleClose() { + setRouteDismissed(true); + closeOnboarding(); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!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 }); + + const adapterConfig = { cwd: rootDir.trim() }; + 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: "claude_local", + adapterConfig, + runtimeConfig, + }); + + // Step 3: Create Engineer agent + await agentsApi.create(company.id, { + name: "Engineer", + role: "engineer", + adapterType: "claude_local", + adapterConfig, + runtimeConfig, + }); + + queryClient.invalidateQueries({ + queryKey: queryKeys.agents.list(company.id), + }); + + // 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 */} +
+ {/* Header */} +
+

+ Welcome to {VOCAB.appName} +

+

+ 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} +

+ )} + + +
+
+
+ + ); +}