import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { CompanyPortabilityCollisionStrategy, CompanyPortabilityFileEntry, CompanyPortabilityPreviewResult, CompanyPortabilitySource, CompanyPortabilityAdapterOverride, } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { authApi } from "../api/auth"; import { companiesApi } from "../api/companies"; import { agentsApi } from "../api/agents"; import { queryKeys } from "../lib/queryKeys"; import { getAgentOrderStorageKey, writeAgentOrder } from "../lib/agent-order"; import { getProjectOrderStorageKey, writeProjectOrder } from "../lib/project-order"; import { MarkdownBody } from "../components/MarkdownBody"; import { Button } from "@/components/ui/button"; import { EmptyState } from "../components/EmptyState"; import { AgentConfigForm } from "../components/AgentConfigForm"; import { cn } from "../lib/utils"; import { ArrowRight, Check, ChevronRight, Download, Github, Package, Upload, } from "lucide-react"; import { Field, adapterLabels } from "../components/agent-config-primitives"; import { defaultCreateValues } from "../components/agent-config-defaults"; import { getUIAdapter, listUIAdapters } from "../adapters"; import type { CreateConfigValues } from "@paperclipai/adapter-utils"; import { type FileTreeNode, type FrontmatterData, buildFileTree, countFiles, collectAllPaths, parseFrontmatter, FRONTMATTER_FIELD_LABELS, PackageFileTree, } from "../components/PackageFileTree"; import { readZipArchive } from "../lib/zip"; import { getPortableFileDataUrl, getPortableFileText, isPortableImageFile } from "../lib/portable-files"; // ── Import-specific helpers ─────────────────────────────────────────── /** Build a map from file path → planned action (create/update/skip) using the manifest + plan */ function buildActionMap(preview: CompanyPortabilityPreviewResult): Map { const map = new Map(); const manifest = preview.manifest; for (const ap of preview.plan.agentPlans) { const agent = manifest.agents.find((a) => a.slug === ap.slug); if (agent) { const path = ensureMarkdownPath(agent.path); map.set(path, ap.action); } } for (const pp of preview.plan.projectPlans) { const project = manifest.projects.find((p) => p.slug === pp.slug); if (project) { const path = ensureMarkdownPath(project.path); map.set(path, pp.action); } } for (const ip of preview.plan.issuePlans) { const issue = manifest.issues.find((i) => i.slug === ip.slug); if (issue) { const path = ensureMarkdownPath(issue.path); map.set(path, ip.action); } } for (const skill of manifest.skills) { const path = ensureMarkdownPath(skill.path); map.set(path, "create"); // Also mark skill file inventory for (const file of skill.fileInventory) { if (preview.files[file.path]) { map.set(file.path, "create"); } } } // Company file if (manifest.company) { const path = ensureMarkdownPath(manifest.company.path); map.set(path, preview.plan.companyAction === "none" ? "skip" : preview.plan.companyAction); } return map; } function ensureMarkdownPath(p: string): string { return p.endsWith(".md") ? p : `${p}.md`; } const ACTION_COLORS: Record = { create: "text-emerald-500 border-emerald-500/30", update: "text-amber-500 border-amber-500/30", overwrite: "text-red-500 border-red-500/30", replace: "text-red-500 border-red-500/30", skip: "text-muted-foreground border-border", none: "text-muted-foreground border-border", }; function FrontmatterCard({ data }: { data: FrontmatterData }) { return (
{Object.entries(data).map(([key, value]) => (
{FRONTMATTER_FIELD_LABELS[key] ?? key}
{Array.isArray(value) ? (
{value.map((item) => ( {item} ))}
) : ( {value} )}
))}
); } // ── Import file tree customization ─────────────────────────────────── function renderImportFileExtra(node: FileTreeNode, checked: boolean, renameMap: Map) { // Show rename indicator only on directories (folders), not individual files const renamedTo = node.kind === "dir" ? renameMap.get(node.path) : undefined; const actionBadge = node.action ? ( {checked ? node.action : "skip"} ) : null; if (!actionBadge && !renamedTo) return null; return ( {renamedTo && checked && ( → {renamedTo} )} {actionBadge} ); } function importFileRowClassName(_node: FileTreeNode, checked: boolean) { return !checked ? "opacity-50" : undefined; } // ── Preview pane ────────────────────────────────────────────────────── function ImportPreviewPane({ selectedFile, content, allFiles, action, renamedTo, }: { selectedFile: string | null; content: CompanyPortabilityFileEntry | null; allFiles: Record; action: string | null; renamedTo: string | null; }) { if (!selectedFile || content === null) { return ( ); } const textContent = getPortableFileText(content); const isMarkdown = selectedFile.endsWith(".md") && textContent !== null; const parsed = isMarkdown && textContent ? parseFrontmatter(textContent) : null; const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null; const actionColor = action ? (ACTION_COLORS[action] ?? ACTION_COLORS.skip) : ""; // Resolve relative image paths within the import package const resolveImageSrc = isMarkdown ? (src: string) => { if (/^(?:https?:|data:)/i.test(src)) return null; const dir = selectedFile.includes("/") ? selectedFile.slice(0, selectedFile.lastIndexOf("/") + 1) : ""; const resolved = dir + src; const entry = allFiles[resolved] ?? allFiles[src]; if (!entry) return null; return getPortableFileDataUrl(resolved in allFiles ? resolved : src, entry); } : undefined; return (
{selectedFile} {renamedTo && ( → {renamedTo} )}
{action && ( {action} )}
{parsed ? ( <> {parsed.body.trim() && {parsed.body}} ) : isMarkdown ? ( {textContent ?? ""} ) : imageSrc ? (
{selectedFile}
) : textContent !== null ? (
            {textContent}
          
) : (
Binary asset preview is not available for this file type.
)}
); } // ── Conflict item type ─────────────────────────────────────────────── interface ConflictItem { slug: string; kind: "agent" | "project" | "issue" | "skill"; originalName: string; plannedName: string; filePath: string | null; action: "rename" | "update"; } function buildConflictList( preview: CompanyPortabilityPreviewResult, ): ConflictItem[] { const conflicts: ConflictItem[] = []; const manifest = preview.manifest; // Agents with collisions for (const ap of preview.plan.agentPlans) { if (ap.existingAgentId) { const agent = manifest.agents.find((a) => a.slug === ap.slug); conflicts.push({ slug: ap.slug, kind: "agent", originalName: agent?.name ?? ap.slug, plannedName: ap.plannedName, filePath: agent ? ensureMarkdownPath(agent.path) : null, action: ap.action === "update" ? "update" : "rename", }); } } // Projects with collisions for (const pp of preview.plan.projectPlans) { if (pp.existingProjectId) { const project = manifest.projects.find((p) => p.slug === pp.slug); conflicts.push({ slug: pp.slug, kind: "project", originalName: project?.name ?? pp.slug, plannedName: pp.plannedName, filePath: project ? ensureMarkdownPath(project.path) : null, action: pp.action === "update" ? "update" : "rename", }); } } return conflicts; } /** Extract a prefix from the import source URL or uploaded zip package name */ function deriveSourcePrefix( sourceMode: string, importUrl: string, localPackageName: string | null, localRootPath: string | null, ): string | null { if (sourceMode === "local") { if (localRootPath) return localRootPath.split("/").pop() ?? null; if (!localPackageName) return null; return localPackageName.replace(/\.zip$/i, "") || null; } if (sourceMode === "github") { const url = importUrl.trim(); if (!url) return null; try { const pathname = new URL(url.startsWith("http") ? url : `https://${url}`).pathname; // For github URLs like /owner/repo/tree/branch/path - take last segment const segments = pathname.split("/").filter(Boolean); return segments.length > 0 ? segments[segments.length - 1] : null; } catch { return null; } } return null; } /** Generate a prefix-based rename: e.g. "gstack" + "CEO" → "gstack-CEO" */ function prefixedName(prefix: string | null, originalName: string): string { if (!prefix) return originalName; return `${prefix}-${originalName}`; } function applyImportedSidebarOrder( preview: CompanyPortabilityPreviewResult | null, result: { company: { id: string }; agents: Array<{ slug: string; id: string | null }>; projects: Array<{ slug: string; id: string | null }>; }, userId: string | null | undefined, ) { const sidebar = preview?.manifest.sidebar; if (!sidebar) return; if (!userId?.trim()) return; const agentIdBySlug = new Map( result.agents .filter((agent): agent is { slug: string; id: string } => typeof agent.id === "string" && agent.id.length > 0) .map((agent) => [agent.slug, agent.id]), ); const projectIdBySlug = new Map( result.projects .filter((project): project is { slug: string; id: string } => typeof project.id === "string" && project.id.length > 0) .map((project) => [project.slug, project.id]), ); const orderedAgentIds = sidebar.agents .map((slug) => agentIdBySlug.get(slug)) .filter((id): id is string => Boolean(id)); const orderedProjectIds = sidebar.projects .map((slug) => projectIdBySlug.get(slug)) .filter((id): id is string => Boolean(id)); if (orderedAgentIds.length > 0) { writeAgentOrder(getAgentOrderStorageKey(result.company.id, userId), orderedAgentIds); } if (orderedProjectIds.length > 0) { writeProjectOrder(getProjectOrderStorageKey(result.company.id, userId), orderedProjectIds); } } // ── Conflict resolution UI ─────────────────────────────────────────── function ConflictResolutionList({ conflicts, nameOverrides, skippedSlugs, confirmedSlugs, onRename, onToggleSkip, onToggleConfirm, }: { conflicts: ConflictItem[]; nameOverrides: Record; skippedSlugs: Set; confirmedSlugs: Set; onRename: (slug: string, newName: string) => void; onToggleSkip: (slug: string, filePath: string | null) => void; onToggleConfirm: (slug: string) => void; }) { if (conflicts.length === 0) return null; return (

Renames

{conflicts.length} item{conflicts.length === 1 ? "" : "s"}
{conflicts.map((item) => { const isSkipped = skippedSlugs.has(item.slug); const isConfirmed = confirmedSlugs.has(item.slug); const currentName = nameOverrides[item.slug] ?? item.plannedName; return (
{/* Skip button on the left */} {item.kind} {item.originalName} {!isSkipped && ( <> {isConfirmed ? ( {currentName} ) : ( onRename(item.slug, e.target.value)} /> )} )} {/* Confirm rename button on the right */} {!isSkipped && ( )}
); })}
); } // ── Adapter type options for import ─────────────────────────────────── const IMPORT_ADAPTER_OPTIONS: { value: string; label: string }[] = listUIAdapters().map((adapter) => ({ value: adapter.type, label: adapterLabels[adapter.type] ?? adapter.label, })); // ── Adapter picker for imported agents ─────────────────────────────── interface AdapterPickerItem { slug: string; name: string; adapterType: string; } function AdapterPickerList({ agents, adapterOverrides, expandedSlugs, configValues, onChangeAdapter, onToggleExpand, onChangeConfig, }: { agents: AdapterPickerItem[]; adapterOverrides: Record; expandedSlugs: Set; configValues: Record; onChangeAdapter: (slug: string, adapterType: string) => void; onToggleExpand: (slug: string) => void; onChangeConfig: (slug: string, patch: Partial) => void; }) { if (agents.length === 0) return null; return (

Adapters

{agents.length} agent{agents.length === 1 ? "" : "s"}
{agents.map((agent) => { const selectedType = adapterOverrides[agent.slug] ?? agent.adapterType; const isExpanded = expandedSlugs.has(agent.slug); const vals = configValues[agent.slug] ?? { ...defaultCreateValues, adapterType: selectedType }; return (
agent {agent.name}
{isExpanded && (
onChangeConfig(agent.slug, patch)} showAdapterTypeField={false} showAdapterTestEnvironmentButton={false} showCreateRunPolicySection={false} hideInstructionsFile sectionLayout="cards" />
)}
); })}
); } // ── Helpers ─────────────────────────────────────────────────────────── async function readLocalPackageZip(file: File): Promise<{ name: string; rootPath: string | null; files: Record; }> { if (!/\.zip$/i.test(file.name)) { throw new Error("Select a .zip company package."); } const archive = await readZipArchive(await file.arrayBuffer()); if (Object.keys(archive.files).length === 0) { throw new Error("No package files were found in the selected zip archive."); } return { name: file.name, rootPath: archive.rootPath, files: archive.files, }; } // ── Main page ───────────────────────────────────────────────────────── export function CompanyImport() { const { selectedCompanyId, selectedCompany, setSelectedCompanyId, } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const packageInputRef = useRef(null); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; // Source state const [sourceMode, setSourceMode] = useState<"github" | "local">("github"); const [importUrl, setImportUrl] = useState(""); const [localPackage, setLocalPackage] = useState<{ name: string; rootPath: string | null; files: Record; } | null>(null); // Target state const [targetMode, setTargetMode] = useState<"existing" | "new">("new"); const [newCompanyName, setNewCompanyName] = useState(""); // Preview state const [importPreview, setImportPreview] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [expandedDirs, setExpandedDirs] = useState>(new Set()); const [checkedFiles, setCheckedFiles] = useState>(new Set()); // Conflict resolution state const [nameOverrides, setNameOverrides] = useState>({}); const [skippedSlugs, setSkippedSlugs] = useState>(new Set()); const [confirmedSlugs, setConfirmedSlugs] = useState>(new Set()); const [collisionStrategy, setCollisionStrategy] = useState("rename"); // Adapter override state const [adapterOverrides, setAdapterOverrides] = useState>({}); const [adapterExpandedSlugs, setAdapterExpandedSlugs] = useState>(new Set()); const [adapterConfigValues, setAdapterConfigValues] = useState>({}); // Fetch current company agents to find CEO adapter type const { data: companyAgents } = useQuery({ queryKey: selectedCompanyId ? queryKeys.agents.list(selectedCompanyId) : ["agents", "none"], queryFn: () => agentsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const ceoAdapterType = useMemo(() => { if (!companyAgents) return "claude_local"; const ceo = companyAgents.find((a) => a.role === "ceo"); return ceo?.adapterType ?? "claude_local"; }, [companyAgents]); const localZipHelpText = "Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly."; useEffect(() => { setBreadcrumbs([ { label: "Org Chart", href: "/org" }, { label: "Import" }, ]); }, [setBreadcrumbs]); function buildSource(): CompanyPortabilitySource | null { if (sourceMode === "local") { if (!localPackage) return null; return { type: "inline", rootPath: localPackage.rootPath, files: localPackage.files }; } const url = importUrl.trim(); if (!url) return null; return { type: "github", url }; } // Preview mutation const previewMutation = useMutation({ mutationFn: () => { const source = buildSource(); if (!source) throw new Error("No source configured."); return companiesApi.importPreview({ source, include: { company: true, agents: true, projects: true, issues: true }, target: targetMode === "new" ? { mode: "new_company", newCompanyName: newCompanyName || null } : { mode: "existing_company", companyId: selectedCompanyId! }, collisionStrategy, }); }, onSuccess: (result) => { setImportPreview(result); // Build conflicts and set default name overrides with prefix const conflicts = buildConflictList(result); const prefix = deriveSourcePrefix( sourceMode, importUrl, localPackage?.name ?? null, localPackage?.rootPath ?? null, ); const defaultOverrides: Record = {}; for (const c of conflicts) { if (c.action === "rename" && prefix) { // Use prefix-based default rename defaultOverrides[c.slug] = prefixedName(prefix, c.originalName); } } setNameOverrides(defaultOverrides); setSkippedSlugs(new Set()); setConfirmedSlugs(new Set()); // Initialize adapter overrides — default all agents to the CEO's adapter type const defaultAdapters: Record = {}; for (const agent of result.manifest.agents) { defaultAdapters[agent.slug] = ceoAdapterType; } setAdapterOverrides(defaultAdapters); setAdapterExpandedSlugs(new Set()); setAdapterConfigValues({}); // Check all files by default, then uncheck COMPANY.md for existing company const allFiles = new Set(Object.keys(result.files)); if (targetMode === "existing" && result.manifest.company && result.plan.companyAction === "update") { const companyPath = ensureMarkdownPath(result.manifest.company.path); allFiles.delete(companyPath); } setCheckedFiles(allFiles); // Expand top-level dirs + all ancestor dirs of files with conflicts (update action) const am = buildActionMap(result); const tree = buildFileTree(result.files, am); const dirsToExpand = new Set(); for (const node of tree) { if (node.kind === "dir") dirsToExpand.add(node.path); } // Auto-expand directories containing conflicting files so they're visible for (const [filePath, action] of am) { if (action === "update") { const segments = filePath.split("/").filter(Boolean); let current = ""; for (let i = 0; i < segments.length - 1; i++) { current = current ? `${current}/${segments[i]}` : segments[i]; dirsToExpand.add(current); } } } setExpandedDirs(dirsToExpand); // Select first file const firstFile = Object.keys(result.files)[0]; if (firstFile) setSelectedFile(firstFile); }, onError: (err) => { pushToast({ tone: "error", title: "Preview failed", body: err instanceof Error ? err.message : "Failed to preview import.", }); }, }); // Build the final nameOverrides to send (only overrides that differ from plannedName) function buildFinalNameOverrides(): Record | undefined { if (!importPreview) return undefined; const overrides: Record = {}; for (const [slug, name] of Object.entries(nameOverrides)) { if (name.trim()) { overrides[slug] = name.trim(); } } return Object.keys(overrides).length > 0 ? overrides : undefined; } function buildSelectedFiles(): string[] | undefined { const selected = Array.from(checkedFiles).sort(); return selected.length > 0 ? selected : undefined; } // Apply mutation const importMutation = useMutation({ mutationFn: () => { const source = buildSource(); if (!source) throw new Error("No source configured."); return companiesApi.importBundle({ source, include: { company: true, agents: true, projects: true, issues: true }, target: targetMode === "new" ? { mode: "new_company", newCompanyName: newCompanyName || null } : { mode: "existing_company", companyId: selectedCompanyId! }, collisionStrategy, nameOverrides: buildFinalNameOverrides(), selectedFiles: buildSelectedFiles(), adapterOverrides: buildFinalAdapterOverrides(), }); }, onSuccess: async (result) => { await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); const importedCompany = await companiesApi.get(result.company.id); const refreshedSession = currentUserId ? null : await queryClient.fetchQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const sidebarOrderUserId = currentUserId ?? refreshedSession?.user?.id ?? refreshedSession?.session?.userId ?? null; applyImportedSidebarOrder(importPreview, result, sidebarOrderUserId); setSelectedCompanyId(importedCompany.id); pushToast({ tone: "success", title: "Import complete", body: `${result.company.name}: ${result.agents.length} agent${result.agents.length === 1 ? "" : "s"} processed.`, }); // Force a fresh dashboard load so newly imported agents are immediately visible. window.location.assign(`/${importedCompany.issuePrefix}/dashboard`); }, onError: (err) => { pushToast({ tone: "error", title: "Import failed", body: err instanceof Error ? err.message : "Failed to apply import.", }); }, }); async function handleChooseLocalPackage(e: ChangeEvent) { const fileList = e.target.files; if (!fileList || fileList.length === 0) return; try { const pkg = await readLocalPackageZip(fileList[0]!); setLocalPackage(pkg); setImportPreview(null); } catch (err) { pushToast({ tone: "error", title: "Package read failed", body: err instanceof Error ? err.message : "Failed to read folder.", }); } } const actionMap = useMemo( () => (importPreview ? buildActionMap(importPreview) : new Map()), [importPreview], ); const tree = useMemo( () => (importPreview ? buildFileTree(importPreview.files, actionMap) : []), [importPreview, actionMap], ); const conflicts = useMemo( () => (importPreview ? buildConflictList(importPreview) : []), [importPreview], ); // Map directory paths → planned rename name for display in the file tree // Also maps file paths for use in the preview header const renameMap = useMemo(() => { const map = new Map(); if (!importPreview) return map; for (const c of conflicts) { if (!c.filePath) continue; const isSkipped = skippedSlugs.has(c.slug); if (isSkipped) continue; const renamedTo = nameOverrides[c.slug] ?? c.plannedName; if (renamedTo === c.originalName) continue; // Map the parent directory (e.g. agents/ceo → gstack-ceo) for the file tree const parentDir = c.filePath.split("/").slice(0, -1).join("/"); if (parentDir) map.set(parentDir, renamedTo); // Map the file path too — used by the preview header, not shown in tree map.set(c.filePath, renamedTo); } return map; }, [importPreview, conflicts, nameOverrides, skippedSlugs]); const totalFiles = useMemo(() => countFiles(tree), [tree]); const selectedCount = checkedFiles.size; function handleToggleDir(path: string) { setExpandedDirs((prev) => { const next = new Set(prev); if (next.has(path)) next.delete(path); else next.add(path); return next; }); } function handleToggleCheck(path: string, kind: "file" | "dir") { if (!importPreview) return; setCheckedFiles((prev) => { const next = new Set(prev); if (kind === "file") { if (next.has(path)) next.delete(path); else next.add(path); } else { const findNode = (nodes: FileTreeNode[], target: string): FileTreeNode | null => { for (const n of nodes) { if (n.path === target) return n; const found = findNode(n.children, target); if (found) return found; } return null; }; const dirNode = findNode(tree, path); if (dirNode) { const childFiles = collectAllPaths(dirNode.children, "file"); for (const child of dirNode.children) { if (child.kind === "file") childFiles.add(child.path); } const allChecked = [...childFiles].every((p) => next.has(p)); for (const f of childFiles) { if (allChecked) next.delete(f); else next.add(f); } } } return next; }); } function handleConflictRename(slug: string, newName: string) { setNameOverrides((prev) => ({ ...prev, [slug]: newName })); // Editing the name un-confirms setConfirmedSlugs((prev) => { if (!prev.has(slug)) return prev; const next = new Set(prev); next.delete(slug); return next; }); } function handleConflictToggleConfirm(slug: string) { setConfirmedSlugs((prev) => { const next = new Set(prev); if (next.has(slug)) next.delete(slug); else next.add(slug); return next; }); } function handleConflictToggleSkip(slug: string, filePath: string | null) { setSkippedSlugs((prev) => { const next = new Set(prev); const wasSkipped = next.has(slug); if (wasSkipped) { next.delete(slug); } else { next.add(slug); } // Sync with file tree checkboxes if (filePath) { setCheckedFiles((prevChecked) => { const nextChecked = new Set(prevChecked); if (wasSkipped) { nextChecked.add(filePath); } else { nextChecked.delete(filePath); } return nextChecked; }); } return next; }); } function handleAdapterChange(slug: string, adapterType: string) { setAdapterOverrides((prev) => ({ ...prev, [slug]: adapterType })); // Reset config values when adapter type changes setAdapterConfigValues((prev) => { const next = { ...prev }; delete next[slug]; return next; }); } function handleAdapterToggleExpand(slug: string) { setAdapterExpandedSlugs((prev) => { const next = new Set(prev); if (next.has(slug)) next.delete(slug); else next.add(slug); return next; }); } function handleAdapterConfigChange(slug: string, patch: Partial) { setAdapterConfigValues((prev) => ({ ...prev, [slug]: { ...(prev[slug] ?? { ...defaultCreateValues, adapterType: adapterOverrides[slug] ?? "claude_local" }), ...patch }, })); } // Build the list of agents for adapter picking const adapterAgents = useMemo(() => { if (!importPreview) return []; return importPreview.manifest.agents.map((a) => ({ slug: a.slug, name: a.name, adapterType: a.adapterType, })); }, [importPreview]); // Build final adapterOverrides for import request function buildFinalAdapterOverrides(): Record | undefined { if (adapterAgents.length === 0) return undefined; const overrides: Record = {}; for (const agent of adapterAgents) { const selectedType = adapterOverrides[agent.slug] ?? agent.adapterType; const configVals = adapterConfigValues[agent.slug]; const override: CompanyPortabilityAdapterOverride = { adapterType: selectedType }; if (configVals) { const uiAdapter = getUIAdapter(selectedType); override.adapterConfig = uiAdapter.buildAdapterConfig(configVals); } overrides[agent.slug] = override; } return Object.keys(overrides).length > 0 ? overrides : undefined; } const hasSource = sourceMode === "local" ? !!localPackage : importUrl.trim().length > 0; const hasErrors = importPreview ? importPreview.errors.length > 0 : false; const previewContent = selectedFile && importPreview ? (() => { return importPreview.files[selectedFile] ?? null; })() : null; const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null; if (!selectedCompanyId) { return ; } return (
{/* Source form section */}

Import source

Choose a GitHub repo or upload a local Paperclip zip package.

{( [ { key: "github", icon: Github, label: "GitHub repo" }, { key: "local", icon: Upload, label: "Local zip" }, ] as const ).map(({ key, icon: Icon, label }) => ( ))}
{sourceMode === "local" ? (
{localPackage && ( {localPackage.name} with{" "} {Object.keys(localPackage.files).length} file {Object.keys(localPackage.files).length === 1 ? "" : "s"} )}
{!localPackage && (

{localZipHelpText}

)}
) : ( { setImportUrl(e.target.value); setImportPreview(null); }} /> )} {targetMode === "new" && ( setNewCompanyName(e.target.value)} placeholder="Imported Company" /> )}
{/* Preview results */} {importPreview && ( <> {/* Sticky import action bar */}
Import preview {selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected {conflicts.length > 0 && ( {conflicts.length} conflict{conflicts.length === 1 ? "" : "s"} )} {importPreview.errors.length > 0 && ( {importPreview.errors.length} error{importPreview.errors.length === 1 ? "" : "s"} )}
{/* Conflict resolution list */} {/* Adapter picker list */} {/* Import button — below renames */}
{/* Warnings */} {importPreview.warnings.length > 0 && (
{importPreview.warnings.map((w) => (
{w}
))}
)} {/* Errors */} {importPreview.errors.length > 0 && (
{importPreview.errors.map((e) => (
{e}
))}
)} {/* Two-column layout */}
)}
); }