import { ChangeEvent, useEffect, useState } from "react"; import { AgentObservabilitySettingsSection } from "../components/observability/AgentObservabilitySettings"; import { useNavigate } from "react-router-dom"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { type InteractionResolverGovernance, type IssueThreadInteractionKind, } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useOptionalToastActions } from "../context/ToastContext"; import { useCloudInstance } from "../hooks/useCloudInstance"; import { resolveCompanyArchiveDeparture } from "../lib/company-selection"; import { cloudPortfolioManageUrl } from "../lib/cloudLinks"; import { navigateTopLevel } from "@/lib/browserNavigation"; import { companiesApi } from "../api/companies"; import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { SlidersHorizontal } from "lucide-react"; import { InteractionGovernancePanel, applyGovernanceChange, type GovernanceField, type GovernanceSelectValue, } from "../components/InteractionGovernancePanel"; import { CompanyPatternIcon } from "../components/CompanyPatternIcon"; import { Field, ToggleField, } from "../components/agent-config-primitives"; import { InstanceGeneralSettings } from "./InstanceGeneralSettings"; export function CompanySettings() { const { companies, selectedCompany, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const toastActions = useOptionalToastActions(); const cloud = useCloudInstance(); // Managed instances derive the task ID prefix from the company name, so a // rename here also renumbers the existing task IDs. const isCloudManaged = Boolean(cloud); // General settings local state const [companyName, setCompanyName] = useState(""); const [description, setDescription] = useState(""); const [logoUrl, setLogoUrl] = useState(""); const [logoUploadError, setLogoUploadError] = useState(null); const [governance, setGovernance] = useState({}); // Sync local state from selected company useEffect(() => { if (!selectedCompany) return; setCompanyName(selectedCompany.name); setDescription(selectedCompany.description ?? ""); setLogoUrl(selectedCompany.logoUrl ?? ""); setGovernance(selectedCompany.interactionResolverGovernance ?? {}); }, [selectedCompany]); const generalDirty = !!selectedCompany && (companyName !== selectedCompany.name || description !== (selectedCompany.description ?? "")); const generalMutation = useMutation({ mutationFn: (data: { name: string; description: 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 governanceMutation = useMutation({ mutationFn: (next: InteractionResolverGovernance) => companiesApi.update(selectedCompanyId!, { interactionResolverGovernance: next }), onSuccess: (company) => { setGovernance(company.interactionResolverGovernance ?? {}); queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); } }); function handleGovernanceChange( kind: IssueThreadInteractionKind, field: GovernanceField, value: GovernanceSelectValue, ) { const next = applyGovernanceChange(governance, kind, field, value); setGovernance(next); governanceMutation.mutate(next); } const syncLogoState = (nextLogoUrl: string | null) => { setLogoUrl(nextLogoUrl ?? ""); void queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); }; const logoUploadMutation = useMutation({ mutationFn: (file: File) => assetsApi .uploadCompanyLogo(selectedCompanyId!, file) .then((asset) => companiesApi.update(selectedCompanyId!, { logoAssetId: asset.assetId })), onSuccess: (company) => { syncLogoState(company.logoUrl); setLogoUploadError(null); } }); const clearLogoMutation = useMutation({ mutationFn: () => companiesApi.update(selectedCompanyId!, { logoAssetId: null }), onSuccess: (company) => { setLogoUploadError(null); syncLogoState(company.logoUrl); } }); function handleLogoFileChange(event: ChangeEvent) { const file = event.target.files?.[0] ?? null; event.currentTarget.value = ""; if (!file) return; setLogoUploadError(null); logoUploadMutation.mutate(file); } function handleClearLogo() { clearLogoMutation.mutate(); } const archiveMutation = useMutation({ mutationFn: ({ companyId }: { companyId: string }) => companiesApi.archive(companyId), onSuccess: async (_result, { companyId }) => { // Never stay on the archived company's settings: the only visible // change would be the archive button going inert. Leave for wherever // still makes sense (another active company, the Cloud portfolio, or // the companies list), with a toast naming what happened. const archived = companies.find((company) => company.id === companyId); const archivedName = archived?.name ?? "Organization"; const departure = resolveCompanyArchiveDeparture({ archivedCompanyId: companyId, companies, cloudPortfolioUrl: cloudPortfolioManageUrl(cloud?.cloudBaseUrl), }); if (departure.kind === "cloud_portfolio") { // The whole organization is on its way to being archived by the // control plane; a full navigation to the Cloud portfolio replaces // this document, so cache invalidation below would never run. navigateTopLevel(departure.url); return; } if (departure.kind === "company") { toastActions?.pushToast({ title: `${archivedName} is archived`, body: `Switched to ${departure.company.name}.`, tone: "info", dedupeKey: `company-archive-departure:${companyId}`, }); setSelectedCompanyId(departure.company.id); navigate(`/${departure.company.issuePrefix}/dashboard`, { replace: true }); } else { toastActions?.pushToast({ title: `${archivedName} is archived`, body: "You can unarchive it from this list.", tone: "info", dedupeKey: `company-archive-departure:${companyId}`, }); navigate(`/${archived?.issuePrefix ?? ""}/companies`, { replace: true }); } 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 organization selected. Select an organization from the switcher above.
); } function handleSaveGeneral() { generalMutation.mutate({ name: companyName.trim(), description: description.trim() || null }); } return (

General

{/* General */}
General
setCompanyName(e.target.value)} /> {isCloudManaged && (

Renaming can change this company's task ID prefix. Existing task IDs are renumbered and old task links stop resolving.

)}
setDescription(e.target.value)} />
{/* Appearance */}
Appearance
{logoUrl && (
)} {(logoUploadMutation.isError || logoUploadError) && ( {logoUploadError ?? (logoUploadMutation.error instanceof Error ? logoUploadMutation.error.message : "Logo upload failed")} )} {clearLogoMutation.isError && ( {clearLogoMutation.error.message} )} {logoUploadMutation.isPending && ( Uploading logo... )}
{/* 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)} toggleTestId="company-settings-team-approval-toggle" />
{/* Interaction governance */} {/* Danger Zone */}
Danger Zone

Archive this organization to hide it from the sidebar. This persists in the database.

{archiveMutation.isError && ( {archiveMutation.error instanceof Error ? archiveMutation.error.message : "Failed to archive organization"} )}
); }