import { useState, useEffect } from "react"; import { VOCAB } from "@paperclipai/branding"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useSearchParams } from "@/lib/router"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { agentsApi } from "../api/agents"; import { companySkillsApi } from "../api/companySkills"; import { queryKeys } from "../lib/queryKeys"; import { AGENT_ROLES } from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Shield } from "lucide-react"; import { cn, agentUrl } from "../lib/utils"; import { roleLabels } from "../components/agent-config-primitives"; import { AgentConfigForm, type CreateConfigValues } from "../components/AgentConfigForm"; import { defaultCreateValues } from "../components/agent-config-defaults"; import { getUIAdapter } from "../adapters"; import { ReportsToPicker } from "../components/ReportsToPicker"; 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"; const SUPPORTED_ADVANCED_ADAPTER_TYPES = new Set([ "claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor", "hermes_local", "openclaw_gateway", ]); function createValuesForAdapterType( adapterType: CreateConfigValues["adapterType"], ): CreateConfigValues { const { adapterType: _discard, ...defaults } = defaultCreateValues; const nextValues: CreateConfigValues = { ...defaults, adapterType }; if (adapterType === "codex_local") { nextValues.model = DEFAULT_CODEX_LOCAL_MODEL; nextValues.dangerouslyBypassSandbox = DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX; } else if (adapterType === "gemini_local") { nextValues.model = DEFAULT_GEMINI_LOCAL_MODEL; } else if (adapterType === "cursor") { nextValues.model = DEFAULT_CURSOR_LOCAL_MODEL; } else if (adapterType === "opencode_local") { nextValues.model = ""; } return nextValues; } export function NewAgent() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const presetAdapterType = searchParams.get("adapterType"); const presetRole = searchParams.get("role"); // [nexus] const presetName = searchParams.get("name"); // [nexus] const [name, setName] = useState(presetName ?? ""); // [nexus] const [title, setTitle] = useState(""); const [role, setRole] = useState(presetRole ?? "general"); // [nexus] const [reportsTo, setReportsTo] = useState(null); const [configValues, setConfigValues] = useState(defaultCreateValues); const [selectedSkillKeys, setSelectedSkillKeys] = useState([]); const [roleOpen, setRoleOpen] = useState(false); const [formError, setFormError] = useState(null); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: adapterModels, error: adapterModelsError, isLoading: adapterModelsLoading, isFetching: adapterModelsFetching, } = useQuery({ queryKey: selectedCompanyId ? queryKeys.agents.adapterModels(selectedCompanyId, configValues.adapterType) : ["agents", "none", "adapter-models", configValues.adapterType], queryFn: () => agentsApi.adapterModels(selectedCompanyId!, configValues.adapterType), enabled: Boolean(selectedCompanyId), }); const { data: companySkills } = useQuery({ queryKey: queryKeys.companySkills.list(selectedCompanyId ?? ""), queryFn: () => companySkillsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const isFirstAgent = !agents || agents.length === 0; const effectiveRole = isFirstAgent ? "ceo" : role; useEffect(() => { setBreadcrumbs([ { label: "Agents", href: "/agents" }, { label: "New Agent" }, ]); }, [setBreadcrumbs]); useEffect(() => { if (isFirstAgent) { if (!name) setName(VOCAB.ceo); if (!title) setTitle(VOCAB.ceo); } }, [isFirstAgent]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { const requested = presetAdapterType; if (!requested) return; if (!SUPPORTED_ADVANCED_ADAPTER_TYPES.has(requested as CreateConfigValues["adapterType"])) { return; } setConfigValues((prev) => { if (prev.adapterType === requested) return prev; return createValuesForAdapterType(requested as CreateConfigValues["adapterType"]); }); }, [presetAdapterType]); const createAgent = useMutation({ mutationFn: (data: Record) => agentsApi.hire(selectedCompanyId!, data), onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) }); navigate(agentUrl(result.agent)); }, onError: (error) => { setFormError(error instanceof Error ? error.message : "Failed to create agent"); }, }); function buildAdapterConfig() { const adapter = getUIAdapter(configValues.adapterType); return adapter.buildAdapterConfig(configValues); } function handleSubmit() { if (!selectedCompanyId || !name.trim()) return; setFormError(null); if (configValues.adapterType === "opencode_local") { const selectedModel = configValues.model.trim(); if (!selectedModel) { setFormError("OpenCode requires an explicit model in provider/model format."); return; } if (adapterModelsError) { setFormError( adapterModelsError instanceof Error ? adapterModelsError.message : "Failed to load OpenCode models.", ); return; } if (adapterModelsLoading || adapterModelsFetching) { setFormError("OpenCode models are still loading. Please wait and try again."); return; } const discovered = adapterModels ?? []; if (!discovered.some((entry) => entry.id === selectedModel)) { setFormError( discovered.length === 0 ? "No OpenCode models discovered. Run `opencode models` and authenticate providers." : `Configured OpenCode model is unavailable: ${selectedModel}`, ); return; } } createAgent.mutate({ name: name.trim(), role: effectiveRole, ...(title.trim() ? { title: title.trim() } : {}), ...(reportsTo ? { reportsTo } : {}), ...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}), adapterType: configValues.adapterType, adapterConfig: buildAdapterConfig(), runtimeConfig: { heartbeat: { enabled: configValues.heartbeatEnabled, intervalSec: configValues.intervalSec, wakeOnDemand: true, cooldownSec: 10, maxConcurrentRuns: 1, }, }, budgetMonthlyCents: 0, }); } const availableSkills = (companySkills ?? []).filter((skill) => !skill.key.startsWith("paperclipai/paperclip/")); function toggleSkill(key: string, checked: boolean) { setSelectedSkillKeys((prev) => { if (checked) { return prev.includes(key) ? prev : [...prev, key]; } return prev.filter((value) => value !== key); }); } return (

New Agent

Advanced agent configuration

{/* Name */}
setName(e.target.value)} autoFocus />
{/* Title */}
setTitle(e.target.value)} />
{/* Property chips: Role + Reports To */}
{AGENT_ROLES.map((r) => ( ))}
{/* Shared config form */} setConfigValues((prev) => ({ ...prev, ...patch }))} adapterModels={adapterModels} />

{VOCAB.company} skills

Optional skills from the {VOCAB.company.toLowerCase()} library. Built-in {VOCAB.appName} runtime skills are added automatically.

{availableSkills.length === 0 ? (

No optional company skills installed yet.

) : (
{availableSkills.map((skill) => { const inputId = `skill-${skill.id}`; const checked = selectedSkillKeys.includes(skill.key); return (
toggleSkill(skill.key, next === true)} />
); })}
)}
{/* Footer */}
{isFirstAgent && (

{`This will be the ${VOCAB.ceo}`}

)} {formError && (

{formError}

)}
); }