import { useState, useRef, useCallback } from "react"; import { Upload, FileText, CheckCircle, AlertTriangle, X, Loader2, FolderOpen } 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; title?: string; skills?: 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"; type InputMethod = "paste" | "file" | "directory"; /** * Modal for importing agents from Agent Companies manifests. * * Supports three input methods: * - File upload (.md/.txt/.sh files) * - Directory upload (webkitdirectory) * - 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 [inputMethod, setInputMethod] = useState("paste"); const [manifestContent, setManifestContent] = useState(""); const [companyName, setCompanyName] = useState("Unknown"); 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 directoryInputRef = useRef(null); const reset = useCallback(() => { setStep("input"); setInputMethod("paste"); setManifestContent(""); setCompanyName("Unknown"); 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; setInputMethod("file"); 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 = ""; }, []); const handleDirectoryChange = useCallback(async (e: React.ChangeEvent) => { const files = Array.from(e.target.files ?? []); if (files.length === 0) return; try { const textFiles = files .filter((file) => /\.(md|txt|sh)$/i.test(file.name)) .sort((a, b) => { const aPath = a.webkitRelativePath || a.name; const bPath = b.webkitRelativePath || b.name; return aPath.localeCompare(bPath); }); if (textFiles.length === 0) { setParseError("Selected directory has no .md, .txt, or .sh files"); return; } const chunks: string[] = []; for (const file of textFiles) { const relativePath = file.webkitRelativePath || file.name; const content = await file.text(); chunks.push(`--- FILE: ${relativePath} ---\n${content}`); } setInputMethod("directory"); setManifestContent(chunks.join("\n\n")); setParseError(null); } catch { setParseError("Failed to read selected directory"); } finally { 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; agents?: AgentPreview[]; created: string[]; skipped: string[]; errors: Array<{ name: string; error: string }>; }; const previewAgents = (data.agents && data.agents.length > 0) ? data.agents : data.created.map((name) => ({ name, role: "custom" })); setCompanyName(data.companyName ?? "Unknown"); 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 an Agent Companies package. Upload an AGENTS.md file, select a directory, or paste manifest content.

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