import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@/lib/router"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { accessApi } from "../api/access"; import { agentsApi } from "../api/agents"; import { adaptersApi } from "../api/adapters"; import { queryKeys } from "@/lib/queryKeys"; import { Dialog, DialogContent, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { ArrowLeft, Bot, Check, MailPlus, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { buildAgentOnboardingPrompt } from "@/lib/agent-onboarding-prompt"; import { listUIAdapters } from "../adapters"; import { isVisualAdapterChoice } from "../adapters/metadata"; import { getAdapterDisplay } from "../adapters/adapter-display-registry"; import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters"; import { useToast } from "../context/ToastContext"; /** * Adapter types that are suitable for agent creation (excludes internal * system adapters like "process" and "http"). */ const SYSTEM_ADAPTER_TYPES = new Set(["process", "http"]); type NewAgentDialogMode = "choices" | "runtime" | "invite" | "prompt"; function isAgentAdapterType(type: string): boolean { return !SYSTEM_ADAPTER_TYPES.has(type); } export function NewAgentDialog() { const { newAgentOpen, closeNewAgent, openNewIssue } = useDialog(); const { selectedCompanyId } = useCompany(); const { pushToast } = useToast(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [mode, setMode] = useState("choices"); const [agentMessage, setAgentMessage] = useState(""); const [latestAgentPrompt, setLatestAgentPrompt] = useState(null); const [latestAgentPromptCopied, setLatestAgentPromptCopied] = useState(false); const disabledTypes = useDisabledAdaptersSync(); function resetDialogState() { setMode("choices"); setAgentMessage(""); setLatestAgentPrompt(null); setLatestAgentPromptCopied(false); } useEffect(() => { if (!latestAgentPromptCopied) return; const timeout = window.setTimeout(() => { setLatestAgentPromptCopied(false); }, 1600); return () => window.clearTimeout(timeout); }, [latestAgentPromptCopied]); // Fetch registered adapters from server (syncs disabled store + provides data) const { data: serverAdapters } = useQuery({ queryKey: queryKeys.adapters.all, queryFn: () => adaptersApi.list(), staleTime: 5 * 60 * 1000, }); // Fetch existing agents for the "Ask CEO" flow const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && newAgentOpen, }); const ceoAgent = (agents ?? []).find((a) => a.role === "ceo"); const inviteHistoryQueryKey = queryKeys.access.invites(selectedCompanyId ?? "", "all", 5); // Build the adapter grid from the UI registry merged with display metadata. // This automatically includes external/plugin adapters. const adapterGrid = useMemo(() => { const registered = listUIAdapters() .filter((a) => isAgentAdapterType(a.type) && !disabledTypes.has(a.type) && isVisualAdapterChoice(a.type) ); // Sort: recommended first, then alphabetical return registered .map((a) => { const display = getAdapterDisplay(a.type); return { value: a.type, label: display.label, desc: display.description, icon: display.icon, recommended: display.recommended, comingSoon: display.comingSoon, disabledLabel: display.disabledLabel, }; }) .sort((a, b) => { if (a.recommended && !b.recommended) return -1; if (!a.recommended && b.recommended) return 1; return a.label.localeCompare(b.label); }); }, [disabledTypes, serverAdapters]); function handleAskCeo() { closeNewAgent(); openNewIssue({ assigneeAgentId: ceoAgent?.id, title: "Create a new agent", description: "(type in what kind of agent you want here)", }); } function handleAdvancedConfig() { setMode("runtime"); } function handleInviteExternalAgent() { setMode("invite"); } function handleAdvancedAdapterPick(adapterType: string) { closeNewAgent(); resetDialogState(); navigate(`/agents/new?adapterType=${encodeURIComponent(adapterType)}`); } async function copyText(text: string, unavailableBody: string) { try { if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } } catch { // Fall through to the unavailable message below. } pushToast({ title: "Clipboard unavailable", body: unavailableBody, tone: "warn", }); return false; } const createAgentInviteMutation = useMutation({ mutationFn: () => accessApi.createCompanyInvite(selectedCompanyId!, { allowedJoinTypes: "agent", humanRole: null, agentMessage: agentMessage.trim() || null, }), onSuccess: async (invite) => { const base = window.location.origin.replace(/\/+$/, ""); const onboardingTextLink = invite.onboardingTextUrl ?? invite.onboardingTextPath ?? `/api/invites/${invite.token}/onboarding.txt`; const onboardingTextUrl = onboardingTextLink.startsWith("http") ? onboardingTextLink : `${base}${onboardingTextLink}`; let prompt: string; try { const manifest = await accessApi.getInviteOnboarding(invite.token); prompt = buildAgentOnboardingPrompt({ onboardingTextUrl, connectionCandidates: manifest.onboarding.connectivity?.connectionCandidates ?? null, testResolutionUrl: manifest.onboarding.connectivity?.testResolutionEndpoint?.url ?? null, }); } catch { prompt = buildAgentOnboardingPrompt({ onboardingTextUrl, connectionCandidates: null, testResolutionUrl: null, }); } setLatestAgentPrompt(prompt); setLatestAgentPromptCopied(false); setMode("prompt"); const copied = await copyText(prompt, "Copy the agent onboarding prompt manually from the field below."); await queryClient.invalidateQueries({ queryKey: inviteHistoryQueryKey }); pushToast({ title: "Agent invite created", body: copied ? "Agent onboarding prompt ready below and copied to clipboard." : "Agent onboarding prompt ready below.", tone: "success", }); }, onError: (error) => { pushToast({ title: "Failed to create agent invite", body: error instanceof Error ? error.message : "Unknown error", tone: "error", }); }, }); return ( { if (!open) { resetDialogState(); closeNewAgent(); } }} > {/* Header */}
Add a new agent
{mode === "choices" ? ( <> {/* Recommendation */}

Ask a leader to propose the hire, configure a runtime yourself, or send an onboarding prompt to an external agent.

(OpenClaw, Hermes, or any agent that can call the invite API.)

) : mode === "runtime" ? ( <>

Choose the runtime Paperclip should start or resume directly.

{adapterGrid.map((opt) => ( ))}
) : mode === "invite" ? (

Invite an external agent

Generate a one-time onboarding prompt that any compatible agent can use to request access, wait for approval, and claim its Paperclip API key.