import { useEffect, useState, useRef, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { AdapterEnvironmentTestResult } from "@paperclipai/shared"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { companiesApi } from "../api/companies"; import { goalsApi } from "../api/goals"; import { agentsApi } from "../api/agents"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { Dialog, DialogPortal } from "@/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { cn } from "../lib/utils"; import { getUIAdapter } from "../adapters"; import { defaultCreateValues } from "./agent-config-defaults"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, DEFAULT_CODEX_LOCAL_MODEL } from "@paperclipai/adapter-codex-local"; import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local"; import { AsciiArtAnimation } from "./AsciiArtAnimation"; import { ChoosePathButton } from "./PathInstructionsModal"; import { HintIcon } from "./agent-config-primitives"; import { Building2, Bot, Code, ListTodo, Rocket, ArrowLeft, ArrowRight, Terminal, Globe, Sparkles, MousePointer2, Check, Loader2, FolderOpen, ChevronDown, X } from "lucide-react"; type Step = 1 | 2 | 3 | 4; type AdapterType = | "claude_local" | "codex_local" | "opencode_local" | "cursor" | "process" | "http" | "openclaw"; const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here: [https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md](https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md) Ensure you have a folder agents/ceo and then download this AGENTS.md as well as the sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file And after you've finished that, hire yourself a Founding Engineer agent`; export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { selectedCompanyId, companies, setSelectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const navigate = useNavigate(); const initialStep = onboardingOptions.initialStep ?? 1; const existingCompanyId = onboardingOptions.companyId; const [step, setStep] = useState(initialStep); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [modelOpen, setModelOpen] = useState(false); // Step 1 const [companyName, setCompanyName] = useState(""); const [companyGoal, setCompanyGoal] = useState(""); // Step 2 const [agentName, setAgentName] = useState("CEO"); const [adapterType, setAdapterType] = useState("claude_local"); const [cwd, setCwd] = useState(""); const [model, setModel] = useState(""); const [command, setCommand] = useState(""); const [args, setArgs] = useState(""); const [url, setUrl] = useState(""); const [adapterEnvResult, setAdapterEnvResult] = useState(null); const [adapterEnvError, setAdapterEnvError] = useState(null); const [adapterEnvLoading, setAdapterEnvLoading] = useState(false); const [forceUnsetAnthropicApiKey, setForceUnsetAnthropicApiKey] = useState(false); const [unsetAnthropicLoading, setUnsetAnthropicLoading] = useState(false); // Step 3 const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md"); const [taskDescription, setTaskDescription] = useState( DEFAULT_TASK_DESCRIPTION ); // Auto-grow textarea for task description const textareaRef = useRef(null); const autoResizeTextarea = useCallback(() => { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; }, []); // Created entity IDs — pre-populate from existing company when skipping step 1 const [createdCompanyId, setCreatedCompanyId] = useState( existingCompanyId ?? null ); const [createdCompanyPrefix, setCreatedCompanyPrefix] = useState< string | null >(null); const [createdAgentId, setCreatedAgentId] = useState(null); const [createdIssueRef, setCreatedIssueRef] = useState(null); // Sync step and company when onboarding opens with options. // Keep this independent from company-list refreshes so Step 1 completion // doesn't get reset after creating a company. useEffect(() => { if (!onboardingOpen) return; const cId = onboardingOptions.companyId ?? null; setStep(onboardingOptions.initialStep ?? 1); setCreatedCompanyId(cId); setCreatedCompanyPrefix(null); }, [ onboardingOpen, onboardingOptions.companyId, onboardingOptions.initialStep ]); // Backfill issue prefix for an existing company once companies are loaded. useEffect(() => { if (!onboardingOpen || !createdCompanyId || createdCompanyPrefix) return; const company = companies.find((c) => c.id === createdCompanyId); if (company) setCreatedCompanyPrefix(company.issuePrefix); }, [onboardingOpen, createdCompanyId, createdCompanyPrefix, companies]); // Resize textarea when step 3 is shown or description changes useEffect(() => { if (step === 3) autoResizeTextarea(); }, [step, taskDescription, autoResizeTextarea]); const { data: adapterModels } = useQuery({ queryKey: ["adapter-models", adapterType], queryFn: () => agentsApi.adapterModels(adapterType), enabled: onboardingOpen && step === 2 }); const isLocalAdapter = adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "opencode_local" || adapterType === "cursor"; const effectiveAdapterCommand = command.trim() || (adapterType === "codex_local" ? "codex" : adapterType === "cursor" ? "agent" : adapterType === "opencode_local" ? "opencode" : "claude"); useEffect(() => { if (step !== 2) return; setAdapterEnvResult(null); setAdapterEnvError(null); }, [step, adapterType, cwd, model, command, args, url]); const selectedModel = (adapterModels ?? []).find((m) => m.id === model); const hasAnthropicApiKeyOverrideCheck = adapterEnvResult?.checks.some( (check) => check.code === "claude_anthropic_api_key_overrides_subscription" ) ?? false; const shouldSuggestUnsetAnthropicApiKey = adapterType === "claude_local" && adapterEnvResult?.status === "fail" && hasAnthropicApiKeyOverrideCheck; function reset() { setStep(1); setLoading(false); setError(null); setCompanyName(""); setCompanyGoal(""); setAgentName("CEO"); setAdapterType("claude_local"); setCwd(""); setModel(""); setCommand(""); setArgs(""); setUrl(""); setAdapterEnvResult(null); setAdapterEnvError(null); setAdapterEnvLoading(false); setForceUnsetAnthropicApiKey(false); setUnsetAnthropicLoading(false); setTaskTitle("Create your CEO HEARTBEAT.md"); setTaskDescription(DEFAULT_TASK_DESCRIPTION); setCreatedCompanyId(null); setCreatedCompanyPrefix(null); setCreatedAgentId(null); setCreatedIssueRef(null); } function handleClose() { reset(); closeOnboarding(); } function buildAdapterConfig(): Record { const adapter = getUIAdapter(adapterType); const config = adapter.buildAdapterConfig({ ...defaultCreateValues, adapterType, cwd, model: adapterType === "codex_local" ? model || DEFAULT_CODEX_LOCAL_MODEL : adapterType === "cursor" ? model || DEFAULT_CURSOR_LOCAL_MODEL : adapterType === "opencode_local" ? model || DEFAULT_OPENCODE_LOCAL_MODEL : model, command, args, url, dangerouslySkipPermissions: adapterType === "claude_local", dangerouslyBypassSandbox: adapterType === "codex_local" ? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX : defaultCreateValues.dangerouslyBypassSandbox }); if (adapterType === "claude_local" && forceUnsetAnthropicApiKey) { const env = typeof config.env === "object" && config.env !== null && !Array.isArray(config.env) ? { ...(config.env as Record) } : {}; env.ANTHROPIC_API_KEY = { type: "plain", value: "" }; config.env = env; } return config; } async function runAdapterEnvironmentTest( adapterConfigOverride?: Record ): Promise { if (!createdCompanyId) { setAdapterEnvError( "Create or select a company before testing adapter environment." ); return null; } setAdapterEnvLoading(true); setAdapterEnvError(null); try { const result = await agentsApi.testEnvironment( createdCompanyId, adapterType, { adapterConfig: adapterConfigOverride ?? buildAdapterConfig() } ); setAdapterEnvResult(result); return result; } catch (err) { setAdapterEnvError( err instanceof Error ? err.message : "Adapter environment test failed" ); return null; } finally { setAdapterEnvLoading(false); } } async function handleStep1Next() { setLoading(true); setError(null); try { const company = await companiesApi.create({ name: companyName.trim() }); setCreatedCompanyId(company.id); setCreatedCompanyPrefix(company.issuePrefix); setSelectedCompanyId(company.id); queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); if (companyGoal.trim()) { await goalsApi.create(company.id, { title: companyGoal.trim(), level: "company", status: "active" }); queryClient.invalidateQueries({ queryKey: queryKeys.goals.list(company.id) }); } setStep(2); } catch (err) { setError(err instanceof Error ? err.message : "Failed to create company"); } finally { setLoading(false); } } async function handleStep2Next() { if (!createdCompanyId) return; setLoading(true); setError(null); try { if (isLocalAdapter) { const result = adapterEnvResult ?? (await runAdapterEnvironmentTest()); if (!result) return; } const agent = await agentsApi.create(createdCompanyId, { name: agentName.trim(), role: "ceo", adapterType, adapterConfig: buildAdapterConfig(), runtimeConfig: { heartbeat: { enabled: true, intervalSec: 3600, wakeOnDemand: true, cooldownSec: 10, maxConcurrentRuns: 1 } } }); setCreatedAgentId(agent.id); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(createdCompanyId) }); setStep(3); } catch (err) { setError(err instanceof Error ? err.message : "Failed to create agent"); } finally { setLoading(false); } } async function handleUnsetAnthropicApiKey() { if (!createdCompanyId || unsetAnthropicLoading) return; setUnsetAnthropicLoading(true); setError(null); setAdapterEnvError(null); setForceUnsetAnthropicApiKey(true); const configWithUnset = (() => { const config = buildAdapterConfig(); const env = typeof config.env === "object" && config.env !== null && !Array.isArray(config.env) ? { ...(config.env as Record) } : {}; env.ANTHROPIC_API_KEY = { type: "plain", value: "" }; config.env = env; return config; })(); try { if (createdAgentId) { await agentsApi.update( createdAgentId, { adapterConfig: configWithUnset }, createdCompanyId ); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(createdCompanyId) }); } const result = await runAdapterEnvironmentTest(configWithUnset); if (result?.status === "fail") { setError( "Retried with ANTHROPIC_API_KEY unset in adapter config, but the environment test is still failing." ); } } catch (err) { setError( err instanceof Error ? err.message : "Failed to unset ANTHROPIC_API_KEY and retry." ); } finally { setUnsetAnthropicLoading(false); } } async function handleStep3Next() { if (!createdCompanyId || !createdAgentId) return; setLoading(true); setError(null); try { const issue = await issuesApi.create(createdCompanyId, { title: taskTitle.trim(), ...(taskDescription.trim() ? { description: taskDescription.trim() } : {}), assigneeAgentId: createdAgentId, status: "todo" }); setCreatedIssueRef(issue.identifier ?? issue.id); queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(createdCompanyId) }); setStep(4); } catch (err) { setError(err instanceof Error ? err.message : "Failed to create task"); } finally { setLoading(false); } } async function handleLaunch() { if (!createdAgentId) return; setLoading(true); setError(null); setLoading(false); reset(); closeOnboarding(); if (createdCompanyPrefix && createdIssueRef) { navigate(`/${createdCompanyPrefix}/issues/${createdIssueRef}`); return; } if (createdCompanyPrefix) { navigate(`/${createdCompanyPrefix}/dashboard`); return; } navigate("/dashboard"); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (step === 1 && companyName.trim()) handleStep1Next(); else if (step === 2 && agentName.trim()) handleStep2Next(); else if (step === 3 && taskTitle.trim()) handleStep3Next(); else if (step === 4) handleLaunch(); } } if (!onboardingOpen) return null; return ( { if (!open) handleClose(); }} > {/* Plain div instead of DialogOverlay — Radix's overlay wraps in RemoveScroll which blocks wheel events on our custom (non-DialogContent) scroll container. A plain div preserves the background without scroll-locking. */}
{/* Close button */} {/* Left half — form */}
{/* Progress indicators */}
Get Started Step {step} of 4
{[1, 2, 3, 4].map((s) => (
))}
{/* Step content */} {step === 1 && (

Name your company

This is the organization your agents will work for.

setCompanyName(e.target.value)} autoFocus />