import { useState, useRef, useCallback } from "react"; import { Upload, FileText, CheckCircle, AlertTriangle, X, Loader2 } from "lucide-react"; export interface AgentImportModalProps { isOpen: boolean; onClose: () => void; onImported: () => void; projectId?: string; } /** Parsed agent preview item for display before import */ interface AgentPreview { name: string; role: string; icon?: string; title?: string; model?: string; } /** Import result from the API */ interface ImportResult { companyName: string; created: Array<{ id: string; name: string }>; skipped: string[]; errors: Array<{ name: string; error: string }>; } /** API error response shape */ interface ApiErrorResponse { error: string; } type ModalStep = "input" | "preview" | "result"; /** * Modal for importing agents from a companies.sh manifest. * * Supports two input methods: * - File upload (.sh files) * - Paste raw manifest content * * Flow: Input โ†’ Preview parsed agents โ†’ Import โ†’ Show results */ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: AgentImportModalProps) { const [step, setStep] = useState("input"); const [manifestContent, setManifestContent] = useState(""); const [companyName, setCompanyName] = useState(""); const [agents, setAgents] = useState([]); const [isParsing, setIsParsing] = useState(false); const [isImporting, setIsImporting] = useState(false); const [parseError, setParseError] = useState(null); const [importResult, setImportResult] = useState(null); const [importError, setImportError] = useState(null); const fileInputRef = useRef(null); const reset = useCallback(() => { setStep("input"); setManifestContent(""); setCompanyName(""); setAgents([]); setIsParsing(false); setIsImporting(false); setParseError(null); setImportResult(null); setImportError(null); }, []); const handleClose = useCallback(() => { reset(); onClose(); }, [reset, onClose]); const handleFileChange = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const content = ev.target?.result as string; setManifestContent(content); setParseError(null); }; reader.onerror = () => { setParseError("Failed to read file"); }; reader.readAsText(file); // Reset file input so the same file can be re-selected e.target.value = ""; }, []); /** Build the API URL with optional projectId */ function buildUrl(path: string): string { if (!projectId) return `/api${path}`; const separator = path.includes("?") ? "&" : "?"; return `/api${path}${separator}projectId=${encodeURIComponent(projectId)}`; } /** Parse the manifest content by calling the API with dryRun=true */ const handleParse = useCallback(async () => { if (!manifestContent.trim()) { setParseError("Please provide manifest content"); return; } setIsParsing(true); setParseError(null); try { const res = await fetch(buildUrl("/agents/import"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ manifest: manifestContent, dryRun: true }), }); if (!res.ok) { const data = await res.json() as ApiErrorResponse; throw new Error(data.error ?? `Parse failed (${res.status})`); } const data = await res.json() as { companyName: string; created: string[]; skipped: string[]; errors: Array<{ name: string; error: string }>; }; // Build agent previews from the created names in the dry-run result // We need to extract more info from the manifest for the preview let previewAgents: AgentPreview[] = []; try { // Parse the base64 manifest directly to get agent details for preview const manifestMatch = manifestContent.match(/AGENT_MANIFEST=["'](.*)["']/); if (manifestMatch) { const decoded = atob(manifestMatch[1]); const parsed = JSON.parse(decoded) as Array>; previewAgents = parsed.map((a) => ({ name: String(a.name ?? ""), role: String(a.role ?? "custom"), icon: a.metadata && typeof a.metadata === "object" ? String((a.metadata as Record).icon ?? "") : undefined, title: a.metadata && typeof a.metadata === "object" ? String((a.metadata as Record).title ?? "") : undefined, model: a.config && typeof a.config === "object" ? String((a.config as Record).model ?? "") : undefined, })); } } catch { // Fallback: just show names from dry-run result previewAgents = data.created.map((name) => ({ name, role: "custom" })); } setCompanyName(data.companyName); setAgents(previewAgents); setStep("preview"); } catch (err) { setParseError(err instanceof Error ? err.message : "Failed to parse manifest"); } finally { setIsParsing(false); } }, [manifestContent, projectId]); /** Execute the actual import */ const handleImport = useCallback(async () => { setIsImporting(true); setImportError(null); try { const res = await fetch(buildUrl("/agents/import"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ manifest: manifestContent, skipExisting: true }), }); if (!res.ok) { const data = await res.json() as ApiErrorResponse; throw new Error(data.error ?? `Import failed (${res.status})`); } const data = await res.json() as ImportResult; setImportResult(data); setStep("result"); onImported(); } catch (err) { setImportError(err instanceof Error ? err.message : "Failed to import agents"); } finally { setIsImporting(false); } }, [manifestContent, projectId, onImported]); if (!isOpen) return null; return (
{ if (e.target === e.currentTarget) handleClose(); }}>
{/* Header */}
Import Agents
{/* Body */}
{/* Step 1: Input */} {step === "input" && (

Import agents from a companies.sh manifest file. Upload a .sh file or paste the manifest content directly.

{/* File upload */}
.sh files supported
{/* Or divider */}
or paste manifest content
{/* Text area for paste */}