import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { CompanyPortabilityCollisionStrategy, CompanyPortabilityExportResult, CompanyPortabilityPreviewRequest, CompanyPortabilityPreviewResult, CompanyPortabilitySource, } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { companiesApi } from "../api/companies"; import { accessApi } from "../api/access"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { Settings, Check, Download, Github, Link2, Upload } from "lucide-react"; import { CompanyPatternIcon } from "../components/CompanyPatternIcon"; import { Field, ToggleField, HintIcon } from "../components/agent-config-primitives"; type AgentSnippetInput = { onboardingTextUrl: string; connectionCandidates?: string[] | null; testResolutionUrl?: string | null; }; export function CompanySettings() { const { companies, selectedCompany, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const packageInputRef = useRef(null); // General settings local state const [companyName, setCompanyName] = useState(""); const [description, setDescription] = useState(""); const [brandColor, setBrandColor] = useState(""); // Sync local state from selected company useEffect(() => { if (!selectedCompany) return; setCompanyName(selectedCompany.name); setDescription(selectedCompany.description ?? ""); setBrandColor(selectedCompany.brandColor ?? ""); }, [selectedCompany]); const [inviteError, setInviteError] = useState(null); const [inviteSnippet, setInviteSnippet] = useState(null); const [snippetCopied, setSnippetCopied] = useState(false); const [snippetCopyDelightId, setSnippetCopyDelightId] = useState(0); const [packageIncludeCompany, setPackageIncludeCompany] = useState(true); const [packageIncludeAgents, setPackageIncludeAgents] = useState(true); const [importSourceMode, setImportSourceMode] = useState<"github" | "url" | "local">("github"); const [importUrl, setImportUrl] = useState(""); const [importTargetMode, setImportTargetMode] = useState<"existing" | "new">("existing"); const [newCompanyName, setNewCompanyName] = useState(""); const [collisionStrategy, setCollisionStrategy] = useState("rename"); const [localPackage, setLocalPackage] = useState<{ rootPath: string | null; files: Record; } | null>(null); const [importPreview, setImportPreview] = useState(null); const generalDirty = !!selectedCompany && (companyName !== selectedCompany.name || description !== (selectedCompany.description ?? "") || brandColor !== (selectedCompany.brandColor ?? "")); const packageInclude = useMemo( () => ({ company: packageIncludeCompany, agents: packageIncludeAgents }), [packageIncludeAgents, packageIncludeCompany] ); const importSource = useMemo(() => { if (importSourceMode === "local") { if (!localPackage || Object.keys(localPackage.files).length === 0) return null; return { type: "inline", rootPath: localPackage.rootPath, files: localPackage.files }; } const trimmed = importUrl.trim(); if (!trimmed) return null; return importSourceMode === "github" ? { type: "github", url: trimmed } : { type: "url", url: trimmed }; }, [importSourceMode, importUrl, localPackage]); const importPayload = useMemo(() => { if (!importSource) return null; return { source: importSource, include: packageInclude, target: importTargetMode === "new" ? { mode: "new_company", newCompanyName: newCompanyName.trim() || null } : { mode: "existing_company", companyId: selectedCompanyId! }, agents: "all", collisionStrategy }; }, [ collisionStrategy, importSource, importTargetMode, newCompanyName, packageInclude, selectedCompanyId ]); const generalMutation = useMutation({ mutationFn: (data: { name: string; description: string | null; brandColor: string | null; }) => companiesApi.update(selectedCompanyId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } }); const settingsMutation = useMutation({ mutationFn: (requireApproval: boolean) => companiesApi.update(selectedCompanyId!, { requireBoardApprovalForNewAgents: requireApproval }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } }); const exportMutation = useMutation({ mutationFn: () => companiesApi.exportBundle(selectedCompanyId!, { include: packageInclude }), onSuccess: async (exported) => { await downloadCompanyPackage(exported); pushToast({ tone: "success", title: "Company package exported", body: `${exported.rootPath}.tar downloaded with ${Object.keys(exported.files).length} file${Object.keys(exported.files).length === 1 ? "" : "s"}.` }); if (exported.warnings.length > 0) { pushToast({ tone: "warn", title: "Export completed with warnings", body: exported.warnings[0] }); } }, onError: (err) => { pushToast({ tone: "error", title: "Export failed", body: err instanceof Error ? err.message : "Failed to export company package" }); } }); const previewImportMutation = useMutation({ mutationFn: (payload: CompanyPortabilityPreviewRequest) => companiesApi.importPreview(payload), onSuccess: (preview) => { setImportPreview(preview); if (preview.errors.length > 0) { pushToast({ tone: "warn", title: "Import preview found issues", body: preview.errors[0] }); return; } pushToast({ tone: "success", title: "Import preview ready", body: `${preview.plan.agentPlans.length} agent action${preview.plan.agentPlans.length === 1 ? "" : "s"} planned.` }); }, onError: (err) => { setImportPreview(null); pushToast({ tone: "error", title: "Import preview failed", body: err instanceof Error ? err.message : "Failed to preview company package" }); } }); const importPackageMutation = useMutation({ mutationFn: (payload: CompanyPortabilityPreviewRequest) => companiesApi.importBundle(payload), onSuccess: async (result) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }), queryClient.invalidateQueries({ queryKey: queryKeys.companies.stats }), queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(result.company.id) }), queryClient.invalidateQueries({ queryKey: queryKeys.org(result.company.id) }) ]); if (importTargetMode === "new") { setSelectedCompanyId(result.company.id); } pushToast({ tone: "success", title: "Company package imported", body: `${result.agents.filter((agent) => agent.action !== "skipped").length} agent${result.agents.filter((agent) => agent.action !== "skipped").length === 1 ? "" : "s"} applied.` }); if (result.warnings.length > 0) { pushToast({ tone: "warn", title: "Import completed with warnings", body: result.warnings[0] }); } setImportPreview(null); setLocalPackage(null); setImportUrl(""); }, onError: (err) => { pushToast({ tone: "error", title: "Import failed", body: err instanceof Error ? err.message : "Failed to import company package" }); } }); const inviteMutation = useMutation({ mutationFn: () => accessApi.createOpenClawInvitePrompt(selectedCompanyId!), onSuccess: async (invite) => { setInviteError(null); const base = window.location.origin.replace(/\/+$/, ""); const onboardingTextLink = invite.onboardingTextUrl ?? invite.onboardingTextPath ?? `/api/invites/${invite.token}/onboarding.txt`; const absoluteUrl = onboardingTextLink.startsWith("http") ? onboardingTextLink : `${base}${onboardingTextLink}`; setSnippetCopied(false); setSnippetCopyDelightId(0); let snippet: string; try { const manifest = await accessApi.getInviteOnboarding(invite.token); snippet = buildAgentSnippet({ onboardingTextUrl: absoluteUrl, connectionCandidates: manifest.onboarding.connectivity?.connectionCandidates ?? null, testResolutionUrl: manifest.onboarding.connectivity?.testResolutionEndpoint?.url ?? null }); } catch { snippet = buildAgentSnippet({ onboardingTextUrl: absoluteUrl, connectionCandidates: null, testResolutionUrl: null }); } setInviteSnippet(snippet); try { await navigator.clipboard.writeText(snippet); setSnippetCopied(true); setSnippetCopyDelightId((prev) => prev + 1); setTimeout(() => setSnippetCopied(false), 2000); } catch { /* clipboard may not be available */ } queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) }); }, onError: (err) => { setInviteError( err instanceof Error ? err.message : "Failed to create invite" ); } }); useEffect(() => { setInviteError(null); setInviteSnippet(null); setSnippetCopied(false); setSnippetCopyDelightId(0); }, [selectedCompanyId]); useEffect(() => { setImportPreview(null); }, [ collisionStrategy, importSourceMode, importTargetMode, importUrl, localPackage, newCompanyName, packageIncludeAgents, packageIncludeCompany, selectedCompanyId ]); const archiveMutation = useMutation({ mutationFn: ({ companyId, nextCompanyId }: { companyId: string; nextCompanyId: string | null; }) => companiesApi.archive(companyId).then(() => ({ nextCompanyId })), onSuccess: async ({ nextCompanyId }) => { if (nextCompanyId) { setSelectedCompanyId(nextCompanyId); } await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); await queryClient.invalidateQueries({ queryKey: queryKeys.companies.stats }); } }); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings" } ]); }, [setBreadcrumbs, selectedCompany?.name]); if (!selectedCompany) { return (
No company selected. Select a company from the switcher above.
); } function handleSaveGeneral() { generalMutation.mutate({ name: companyName.trim(), description: description.trim() || null, brandColor: brandColor || null }); } async function handleChooseLocalPackage( event: ChangeEvent ) { const selection = event.target.files; if (!selection || selection.length === 0) { setLocalPackage(null); return; } try { const parsed = await readLocalPackageSelection(selection); setLocalPackage(parsed); pushToast({ tone: "success", title: "Local package loaded", body: `${Object.keys(parsed.files).length} markdown file${Object.keys(parsed.files).length === 1 ? "" : "s"} ready for preview.` }); } catch (err) { setLocalPackage(null); pushToast({ tone: "error", title: "Failed to read local package", body: err instanceof Error ? err.message : "Could not read selected files" }); } finally { event.target.value = ""; } } function handlePreviewImport() { if (!importPayload) { pushToast({ tone: "warn", title: "Source required", body: importSourceMode === "local" ? "Choose a local folder with COMPANY.md before previewing." : "Enter a company package URL before previewing." }); return; } previewImportMutation.mutate(importPayload); } function handleApplyImport() { if (!importPayload) { pushToast({ tone: "warn", title: "Source required", body: importSourceMode === "local" ? "Choose a local folder with COMPANY.md before importing." : "Enter a company package URL before importing." }); return; } importPackageMutation.mutate(importPayload); } return (

Company Settings

{/* General */}
General
setCompanyName(e.target.value)} /> setDescription(e.target.value)} />
{/* Appearance */}
Appearance
setBrandColor(e.target.value)} className="h-8 w-8 cursor-pointer rounded border border-border bg-transparent p-0" /> { const v = e.target.value; if (v === "" || /^#[0-9a-fA-F]{0,6}$/.test(v)) { setBrandColor(v); } }} placeholder="Auto" className="w-28 rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm font-mono outline-none" /> {brandColor && ( )}
{/* Save button for General + Appearance */} {generalDirty && (
{generalMutation.isSuccess && ( Saved )} {generalMutation.isError && ( {generalMutation.error instanceof Error ? generalMutation.error.message : "Failed to save"} )}
)} {/* Hiring */}
Hiring
settingsMutation.mutate(v)} />
{/* Invites */}
Invites
Generate an OpenClaw agent invite snippet.
{inviteError && (

{inviteError}

)} {inviteSnippet && (
OpenClaw Invite Prompt
{snippetCopied && ( Copied )}