import { useEffect, useState, useRef, useCallback, useMemo } 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 { extractModelName, extractProviderIdWithFallback } from "../lib/model-utils"; import { getUIAdapter } from "../adapters"; import { defaultCreateValues } from "./agent-config-defaults"; import { parseOnboardingGoalInput } from "../lib/onboarding-goal"; 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_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { OnboardingChat } from "./OnboardingChat"; import { AsciiArtAnimation } from "./AsciiArtAnimation"; import { ChoosePathButton } from "./PathInstructionsModal"; import { HintIcon } from "./agent-config-primitives"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; import { FrontDoor } from "./FrontDoor"; import { Building2, Bot, Code, Gem, ListTodo, Rocket, ArrowLeft, ArrowRight, Terminal, Sparkles, MousePointer2, Check, Loader2, FolderOpen, ChevronDown, X, Plus, Pencil, Trash2, MessageSquare } from "lucide-react"; type Step = 0 | 1 | 2 | 3 | 4 | 5 | 6; type AdapterType = | "claude_local" | "codex_local" | "gemini_local" | "opencode_local" | "pi_local" | "cursor" | "process" | "http" | "openclaw_gateway"; const MISSION_PROMPT_CHIPS = [ "Build a SaaS product", "Scale a content business", "Launch a marketplace" ]; function buildMissionFromQuestionnaire(q1: string, q2: string, q3: string, q4: string): string { const parts: string[] = []; if (q1.trim()) parts.push(q1.trim()); if (q2.trim()) parts.push(`We serve ${q2.trim().toLowerCase()}.`); if (q3.trim()) parts.push(`Our biggest challenge is ${q3.trim().toLowerCase()}.`); if (q4.trim()) parts.push(`Success looks like ${q4.trim().toLowerCase()}.`); return parts.join(" "); } interface HiringRole { id: string; name: string; summary: string; expertise: string; priorities: string; boundaries: string; tools: string; communication: string; collaboration: string; enabled: boolean; editing: boolean; } function nextRoleId(): string { return crypto.randomUUID(); } const EMPTY_ROLE: Omit = { name: "", summary: "", expertise: "", priorities: "", boundaries: "", tools: "", communication: "", collaboration: "", enabled: true, editing: true, }; function cleanMd(s: string): string { return s .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/^\s*[-*]\s+/, "") .trim(); } /** * Map a bullet label (e.g. "Why:", "Responsibilities:") to a structured field. */ function classifyBullet(label: string): keyof HiringRole | null { const l = label.toLowerCase(); if (/^why|^purpose|^overview/.test(l)) return "summary"; if (/^responsibilit|^expertise|^duties|^scope|^what they do/.test(l)) return "expertise"; if (/^priorit|^focus|^goals|^kpi|^metric/.test(l)) return "priorities"; if (/^boundar|^limit|^should not|^don.?t|^avoid|^out of scope/.test(l)) return "boundaries"; if (/^tool|^permission|^access|^tech|^stack/.test(l)) return "tools"; if (/^communic|^tone|^style|^voice/.test(l)) return "communication"; if (/^collaborat|^escalat|^report|^works with|^interact|^coordinat/.test(l)) return "collaboration"; if (/^recommend|^profile|^ideal|^skills|^qualif/.test(l)) return "expertise"; return null; } /** * Parse a markdown hiring plan into structured roles. * Handles two document formats: * Format A: "## Role N: Name" with ### sub-sections (Priorities, Boundaries, etc.) * Format B: "### N. Name" with **Label:** bullets * Fallback: comment-style bullet/table patterns. */ function parseHiringPlan(markdown: string): HiringRole[] { const roles: HiringRole[] = []; const seen = new Set(); // Find role headings: any ## or ### heading with a numbered prefix // like "### 1. Content Marketing Officer" or "## Role 2: CTO" const rolePattern = /^(?:role\s*\d+[:.]\s*|\d+[.)]\s*)/i; const roleHeadingRegex = /^#{2,3}\s+(.+)$/gm; let match: RegExpExecArray | null; // First pass: find all role heading positions (start of line, end of heading) const rolePositions: Array<{ title: string; lineStart: number; contentStart: number }> = []; while ((match = roleHeadingRegex.exec(markdown)) !== null) { if (rolePattern.test(match[1].trim())) { rolePositions.push({ title: match[1].trim(), lineStart: match.index, contentStart: match.index + match[0].length, }); } } // Extract body for each role (from heading end to the next role heading start) const sections: Array<{ title: string; body: string }> = []; for (let i = 0; i < rolePositions.length; i++) { const end = i + 1 < rolePositions.length ? rolePositions[i + 1].lineStart : markdown.length; sections.push({ title: rolePositions[i].title, body: markdown.slice(rolePositions[i].contentStart, end), }); } for (const section of sections) { if (!rolePattern.test(section.title)) continue; let name = section.title .replace(/^role\s*\d*[:.]\s*/i, "") .replace(/^\d+[.)]\s*/, "") .replace(/\*\*/g, "") .trim(); if (name.length < 3) continue; if (seen.has(name.toLowerCase())) continue; // Parse content: **Label:** bullets and ### sub-sections const fields: Record = {}; let currentField: string | null = null; const bodyLines = section.body.split("\n"); for (let i = 0; i < bodyLines.length; i++) { const raw = bodyLines[i]; const trimmed = raw.trim(); if (!trimmed) continue; // ### sub-section heading (e.g. "### Priorities") const subHeadingMatch = trimmed.match(/^###\s+(.+)/); if (subHeadingMatch) { const label = subHeadingMatch[1].trim(); const field = classifyBullet(label); currentField = (field && field !== "id" && field !== "name" && field !== "enabled" && field !== "editing") ? field : "expertise"; continue; } // **Label:** inline (e.g. "**Why:** text") const boldLabelMatch = trimmed.match(/^\*\*([^*:]+)[*:]*\*\*[:\s]*(.*)/); const bulletLabelMatch = !boldLabelMatch && trimmed.match(/^\s*[-*]\s+\*\*([^*:]+)[*:]*\*\*[:\s]*(.*)/); const labelMatch = boldLabelMatch ?? bulletLabelMatch; if (labelMatch) { const label = labelMatch[1]!.trim(); const value = cleanMd(labelMatch[2] ?? ""); const field = classifyBullet(label); currentField = (field && field !== "id" && field !== "name" && field !== "enabled" && field !== "editing") ? field : "expertise"; if (!fields[currentField]) fields[currentField] = []; if (value) fields[currentField].push(value); continue; } // Regular content line under current field if (currentField) { const cleaned = cleanMd(trimmed); if (cleaned) { if (!fields[currentField]) fields[currentField] = []; fields[currentField].push(cleaned); } } } const join = (arr?: string[]) => (arr ?? []).join("\n"); // If no summary, use first line of expertise let summary = join(fields.summary); let expertise = join(fields.expertise); if (!summary && expertise) { const lines = expertise.split("\n"); summary = lines[0]; expertise = lines.slice(1).join("\n"); } seen.add(name.toLowerCase()); roles.push({ id: nextRoleId(), name, summary, expertise, priorities: join(fields.priorities), boundaries: join(fields.boundaries), tools: join(fields.tools), communication: join(fields.communication), collaboration: join(fields.collaboration), enabled: true, editing: false, }); } // Fallback: parse "N. **Role Name**" with indented bullets if (roles.length === 0) { const lines = markdown.split("\n"); let currentRole: HiringRole | null = null; for (const line of lines) { // Match numbered bold role: "1. **Content Strategist / CMO**" const roleMatch = line.match(/^\s*(\d+)[.)]\s+\*\*([^*]+)\*\*/); if (roleMatch) { const name = roleMatch[2].trim(); const skip = /^(phase|month|step|update|note|question|summary|timeline|priority|plan|total|budget|immediate|hire)/i; if (skip.test(name) || name.length < 3) continue; if (seen.has(name.toLowerCase())) continue; if (currentRole) roles.push(currentRole); seen.add(name.toLowerCase()); currentRole = { id: nextRoleId(), name, summary: "", expertise: "", priorities: "", boundaries: "", tools: "", communication: "", collaboration: "", enabled: true, editing: false, }; continue; } // Indented bullets under the current role if (currentRole && /^\s{2,}[-*]/.test(line)) { const cleaned = cleanMd(line); if (!cleaned) continue; // Check for labeled bullet: "*Why first:*", "**Tools:**", etc. const labelMatch = cleaned.match(/^\*?([^:*]+)\*?:\s*(.*)/); if (labelMatch) { const field = classifyBullet(labelMatch[1].trim()); if (field && typeof currentRole[field] === "string") { const val = labelMatch[2].trim(); const prev = currentRole[field] as string; (currentRole as unknown as Record)[field] = prev ? `${prev}\n${val}` : val; continue; } } // Default: add to expertise currentRole.expertise = currentRole.expertise ? `${currentRole.expertise}\n${cleaned}` : cleaned; } } if (currentRole) roles.push(currentRole); // If summary is empty, use the first line of expertise as the summary for (const role of roles) { if (!role.summary && role.expertise) { const firstLine = role.expertise.split("\n")[0]; if (firstLine) { role.summary = firstLine; role.expertise = role.expertise.split("\n").slice(1).join("\n"); } } } } return roles; } 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 Ensure you have a folder agents/ceo and then download this AGENTS.md, and sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file After that, hire yourself a Founding Engineer agent and then plan the roadmap and tasks for your new company.`; const ONBOARDING_STORAGE_KEY = "paperclip-onboarding-state"; function loadSavedState(): Record | null { try { const raw = localStorage.getItem(ONBOARDING_STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { selectedCompanyId, companies, setSelectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const navigate = useNavigate(); const initialStep = onboardingOptions.initialStep ?? 0; const existingCompanyId = onboardingOptions.companyId; // Restore saved state from localStorage (read once on mount) const saved = useMemo(loadSavedState, []); const [step, setStep] = useState((saved?.step as Step) ?? initialStep); const [onboardingPath, setOnboardingPath] = useState<"create" | "grow" | null>((saved?.onboardingPath as "create" | "grow" | null) ?? null); // "Grow existing" questionnaire fields const [growWorkflows, setGrowWorkflows] = useState((saved?.growWorkflows as string) ?? ""); const [growPainPoints, setGrowPainPoints] = useState((saved?.growPainPoints as string) ?? ""); const [growAutomate, setGrowAutomate] = useState((saved?.growAutomate as string) ?? ""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [modelOpen, setModelOpen] = useState(false); const [modelSearch, setModelSearch] = useState(""); // Step 1 const [companyName, setCompanyName] = useState((saved?.companyName as string) ?? ""); const [companyGoal, setCompanyGoal] = useState((saved?.companyGoal as string) ?? ""); const [missionPath, setMissionPath] = useState<"direct" | "questionnaire" | null>((saved?.missionPath as "direct" | "questionnaire" | null) ?? null); const [missionConfirmed, setMissionConfirmed] = useState((saved?.missionConfirmed as boolean) ?? false); // Questionnaire answers const [q1, setQ1] = useState((saved?.q1 as string) ?? ""); // What do you do? const [q2, setQ2] = useState((saved?.q2 as string) ?? ""); // Who do you serve? const [q3, setQ3] = useState((saved?.q3 as string) ?? ""); // Biggest bottleneck? const [q4, setQ4] = useState((saved?.q4 as string) ?? ""); // What would success look like? // Step 2 const [agentName, setAgentName] = useState((saved?.agentName as string) ?? "CEO"); const [adapterType, setAdapterType] = useState((saved?.adapterType as AdapterType) ?? "claude_local"); const [cwd, setCwd] = useState((saved?.cwd as string) ?? ""); const [model, setModel] = useState((saved?.model as string) ?? ""); const [command, setCommand] = useState((saved?.command as string) ?? ""); const [args, setArgs] = useState((saved?.args as string) ?? ""); const [url, setUrl] = useState((saved?.url as string) ?? ""); 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); const [showMoreAdapters, setShowMoreAdapters] = 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"; }, []); // Planning task + hiring plan const [planningTaskId, setPlanningTaskId] = useState((saved?.planningTaskId as string) ?? null); const [planContent, setPlanContent] = useState((saved?.planContent as string) ?? null); const [hiringRoles, setHiringRoles] = useState((saved?.hiringRoles as HiringRole[]) ?? []); const [showRawPlan, setShowRawPlan] = useState(false); // Created entity IDs — pre-populate from existing company when skipping step 1 const [createdCompanyId, setCreatedCompanyId] = useState( existingCompanyId ?? (saved?.createdCompanyId as string) ?? null ); const [createdCompanyPrefix, setCreatedCompanyPrefix] = useState< string | null >((saved?.createdCompanyPrefix as string) ?? null); const [createdAgentId, setCreatedAgentId] = useState((saved?.createdAgentId as string) ?? null); const [createdIssueRef, setCreatedIssueRef] = useState(null); // Sync step and company when onboarding opens with explicit options. // Only override saved state when onboardingOptions explicitly provides values. useEffect(() => { if (!onboardingOpen) return; // If explicit options are provided, they take precedence over saved state if (onboardingOptions.initialStep) { setStep(onboardingOptions.initialStep); } if (onboardingOptions.companyId) { setCreatedCompanyId(onboardingOptions.companyId); 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]); // Persist wizard state to localStorage on every change useEffect(() => { if (!onboardingOpen) return; const state = { step, companyName, companyGoal, missionPath, missionConfirmed, q1, q2, q3, q4, agentName, adapterType, cwd, model, command, args, url, createdCompanyId, createdCompanyPrefix, createdAgentId, planningTaskId, planContent, hiringRoles, onboardingPath, growWorkflows, growPainPoints, growAutomate, }; localStorage.setItem(ONBOARDING_STORAGE_KEY, JSON.stringify(state)); }, [ onboardingOpen, step, companyName, companyGoal, missionPath, missionConfirmed, q1, q2, q3, q4, agentName, adapterType, cwd, model, command, args, url, createdCompanyId, createdCompanyPrefix, createdAgentId, planningTaskId, planContent, hiringRoles, onboardingPath, growWorkflows, growPainPoints, growAutomate, ]); // Resize textarea when step 3 is shown or description changes useEffect(() => { // Auto-resize removed — task description textarea no longer used in onboarding }, [step, autoResizeTextarea]); const { data: adapterModels, error: adapterModelsError, isLoading: adapterModelsLoading, isFetching: adapterModelsFetching } = useQuery({ queryKey: createdCompanyId ? queryKeys.agents.adapterModels(createdCompanyId, adapterType) : ["agents", "none", "adapter-models", adapterType], queryFn: () => agentsApi.adapterModels(createdCompanyId!, adapterType), enabled: Boolean(createdCompanyId) && onboardingOpen && step === 3 }); const isLocalAdapter = adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "gemini_local" || adapterType === "opencode_local" || adapterType === "cursor"; const effectiveAdapterCommand = command.trim() || (adapterType === "codex_local" ? "codex" : adapterType === "gemini_local" ? "gemini" : adapterType === "cursor" ? "agent" : adapterType === "opencode_local" ? "opencode" : "claude"); useEffect(() => { if (step !== 3) 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; const filteredModels = useMemo(() => { const query = modelSearch.trim().toLowerCase(); return (adapterModels ?? []).filter((entry) => { if (!query) return true; const provider = extractProviderIdWithFallback(entry.id, ""); return ( entry.id.toLowerCase().includes(query) || entry.label.toLowerCase().includes(query) || provider.toLowerCase().includes(query) ); }); }, [adapterModels, modelSearch]); const groupedModels = useMemo(() => { if (adapterType !== "opencode_local") { return [ { provider: "models", entries: [...filteredModels].sort((a, b) => a.id.localeCompare(b.id)) } ]; } const groups = new Map>(); for (const entry of filteredModels) { const provider = extractProviderIdWithFallback(entry.id); const bucket = groups.get(provider) ?? []; bucket.push(entry); groups.set(provider, bucket); } return Array.from(groups.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([provider, entries]) => ({ provider, entries: [...entries].sort((a, b) => a.id.localeCompare(b.id)) })); }, [filteredModels, adapterType]); function reset() { localStorage.removeItem(ONBOARDING_STORAGE_KEY); setStep(0); setOnboardingPath(null); setGrowWorkflows(""); setGrowPainPoints(""); setGrowAutomate(""); setLoading(false); setError(null); setCompanyName(""); setCompanyGoal(""); setMissionPath(null); setMissionConfirmed(false); setQ1(""); setQ2(""); setQ3(""); setQ4(""); setPlanningTaskId(null); setPlanContent(null); setHiringRoles([]); setShowRawPlan(false); 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 handleLaunchToChat() { const prefix = createdCompanyPrefix; reset(); closeOnboarding(); navigate(prefix ? `/${prefix}/board-chat` : "/dashboard"); } function buildAdapterConfig(): Record { const adapter = getUIAdapter(adapterType); const config = adapter.buildAdapterConfig({ ...defaultCreateValues, adapterType, cwd, model: adapterType === "codex_local" ? model || DEFAULT_CODEX_LOCAL_MODEL : adapterType === "gemini_local" ? model || DEFAULT_GEMINI_LOCAL_MODEL : adapterType === "cursor" ? model || DEFAULT_CURSOR_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 }); const parsedGoal = parseOnboardingGoalInput(companyGoal); await goalsApi.create(company.id, { title: parsedGoal.title, ...(parsedGoal.description ? { description: parsedGoal.description } : {}), level: "company", status: "active" }); queryClient.invalidateQueries({ queryKey: queryKeys.goals.list(company.id) }); setStep(2); // → CEO config (was celebration, now swapped) } 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 (adapterType === "opencode_local") { const selectedModelId = model.trim(); if (!selectedModelId) { setError( "OpenCode requires an explicit model in provider/model format." ); return; } if (adapterModelsError) { setError( adapterModelsError instanceof Error ? adapterModelsError.message : "Failed to load OpenCode models." ); return; } if (adapterModelsLoading || adapterModelsFetching) { setError( "OpenCode models are still loading. Please wait and try again." ); return; } const discoveredModels = adapterModels ?? []; if (!discoveredModels.some((entry) => entry.id === selectedModelId)) { setError( discoveredModels.length === 0 ? "No OpenCode models discovered. Run `opencode models` and authenticate providers." : `Configured OpenCode model is unavailable: ${selectedModelId}` ); return; } } 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) }); // Create the planning task unassigned — the CEO only gets assigned // when the user sends their first message (user controls initiation) const isGrowPath = onboardingPath === "grow"; const growContext = isGrowPath ? `\n\nExisting workflows: ${growWorkflows}\nPain points: ${growPainPoints}\nFirst automation priority: ${growAutomate}` : ""; const planningIssue = await issuesApi.create(createdCompanyId, { title: isGrowPath ? "Plan AI agents for existing company" : "Strategy & hiring plan with CEO", description: `Company mission: ${companyGoal}${growContext} You are the CEO of this company. The board (the user) has just appointed you. Your first conversation should focus on STRATEGY — ask the board about their vision, priorities, and constraints. DO NOT immediately create a hiring plan. Instead: 1. FIRST: Greet the board briefly, then ask strategic questions to understand their priorities. Have a real conversation. 2. ONLY WHEN the board says they're ready (e.g. "let's build the plan", "get started", "hire the team"), THEN create the hiring plan document. 3. When you do create the plan, save it as a document using the plan key.${isGrowPath ? "\n4. Focus on agents that address the existing pain points and automate current workflows." : ""} When writing the hiring plan document, use this exact format for EACH role. Use ## headings for each role (e.g. "## 1. Role Name") and ### sub-headings for each section within the role: ## 1. Role Name ### Summary One-line description of this role. ### Expertise & Responsibilities What this agent does, detailed responsibilities. ### Priorities Ordered list of what matters most. ### Boundaries What this role should NOT do. ### Tools & Permissions What tools and access this role needs. ### Communication Tone, style, and interaction guidelines. ### Collaboration & Escalation Who this role works with, escalation paths. Follow this structure for every role in the plan.`, status: "backlog" }); setPlanningTaskId(planningIssue.id); // Go to launch celebration step (step 3) 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; setError(null); setStep(4); } async function handleLaunch() { if (!createdCompanyId || !createdAgentId) return; setLoading(true); setError(null); try { // Create a hire task for each approved role const approvedRoles = hiringRoles.filter( (r) => r.enabled && r.name.trim() ); for (const role of approvedRoles) { const roleSpec = [ role.summary && `**Summary:** ${role.summary}`, role.expertise && `**Expertise & Responsibilities:**\n${role.expertise}`, role.priorities && `**Priorities:**\n${role.priorities}`, role.boundaries && `**Boundaries:**\n${role.boundaries}`, role.tools && `**Tools & Permissions:**\n${role.tools}`, role.communication && `**Communication:**\n${role.communication}`, role.collaboration && `**Collaboration:**\n${role.collaboration}`, ].filter(Boolean).join("\n\n"); await issuesApi.create(createdCompanyId, { title: `Hire: ${role.name}`, description: `Hire a ${role.name} for the company.\n\n${roleSpec}`, assigneeAgentId: createdAgentId, status: "todo" }); } queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(createdCompanyId) }); setSelectedCompanyId(createdCompanyId); setStep(6); // → orientation screen } catch (err) { setError(err instanceof Error ? err.message : "Failed to create hire tasks"); } finally { setLoading(false); } } function handleFinishOnboarding() { const prefix = createdCompanyPrefix; reset(); // clears localStorage closeOnboarding(); navigate(prefix ? `/${prefix}/dashboard` : `/dashboard`); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (step === 0) return; // front door requires click if (step === 1 && companyName.trim() && companyGoal.trim()) handleStep1Next(); else if (step === 2 && agentName.trim()) handleStep2Next(); else if (step === 3) handleLaunchToChat(); else if (step === 4) setStep(5); else if (step === 5) setStep(6); else if (step === 6) 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 */} {/* Step 0: Front Door — full-screen choice */} {step === 0 && (
{ setOnboardingPath(path); setStep(1); }} />
)} {/* Left half — form (steps 1+) */} {step !== 0 && (
{/* Progress tabs */}
{( [ { step: 1 as Step, label: "Mission", icon: Building2 }, { step: 2 as Step, label: "CEO", icon: Bot }, { step: 3 as Step, label: "Launch", icon: Rocket }, ] as const ).map(({ step: s, label, icon: Icon }) => ( ))}
{/* Step content */} {step === 1 && onboardingPath === "grow" && (

Tell us about your company

We'll use this to configure your CEO and plan which agents to add.

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