import { ChangeEvent, useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AGENT_ADAPTER_TYPES, getAdapterEnvironmentSupport, type Environment, type EnvironmentProbeResult, } 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 { assetsApi } from "../api/assets"; import { environmentsApi } from "../api/environments"; import { instanceSettingsApi } from "../api/instanceSettings"; import { secretsApi } from "../api/secrets"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { Settings, Check, Download, Upload } from "lucide-react"; import { CompanyPatternIcon } from "../components/CompanyPatternIcon"; import { Field, ToggleField, HintIcon, adapterLabels, } from "../components/agent-config-primitives"; type AgentSnippetInput = { onboardingTextUrl: string; connectionCandidates?: string[] | null; testResolutionUrl?: string | null; }; type EnvironmentFormState = { name: string; description: string; driver: "local" | "ssh" | "sandbox"; sshHost: string; sshPort: string; sshUsername: string; sshRemoteWorkspacePath: string; sshPrivateKey: string; sshPrivateKeySecretId: string; sshKnownHosts: string; sshStrictHostKeyChecking: boolean; sandboxProvider: string; sandboxImage: string; sandboxTemplate: string; sandboxApiKey: string; sandboxApiKeySecretId: string; sandboxTimeoutMs: string; sandboxReuseLease: boolean; }; const ENVIRONMENT_SUPPORT_ROWS = AGENT_ADAPTER_TYPES.map((adapterType) => ({ adapterType, support: getAdapterEnvironmentSupport(adapterType), })); function buildEnvironmentPayload(form: EnvironmentFormState) { return { name: form.name.trim(), description: form.description.trim() || null, driver: form.driver, config: form.driver === "ssh" ? { host: form.sshHost.trim(), port: Number.parseInt(form.sshPort || "22", 10) || 22, username: form.sshUsername.trim(), remoteWorkspacePath: form.sshRemoteWorkspacePath.trim(), privateKey: form.sshPrivateKey.trim() || null, privateKeySecretRef: form.sshPrivateKey.trim().length > 0 || !form.sshPrivateKeySecretId ? null : { type: "secret_ref" as const, secretId: form.sshPrivateKeySecretId, version: "latest" as const }, knownHosts: form.sshKnownHosts.trim() || null, strictHostKeyChecking: form.sshStrictHostKeyChecking, } : form.driver === "sandbox" ? { provider: form.sandboxProvider.trim(), image: form.sandboxImage.trim() || "ubuntu:24.04", timeoutMs: Number.parseInt(form.sandboxTimeoutMs || "300000", 10) || 300000, reuseLease: form.sandboxReuseLease, } : {}, } as const; } function createEmptyEnvironmentForm(): EnvironmentFormState { return { name: "", description: "", driver: "ssh", sshHost: "", sshPort: "22", sshUsername: "", sshRemoteWorkspacePath: "", sshPrivateKey: "", sshPrivateKeySecretId: "", sshKnownHosts: "", sshStrictHostKeyChecking: true, sandboxProvider: "", sandboxImage: "ubuntu:24.04", sandboxTemplate: "base", sandboxApiKey: "", sandboxApiKeySecretId: "", sandboxTimeoutMs: "300000", sandboxReuseLease: false, }; } function readSshConfig(environment: Environment) { const config = environment.config ?? {}; return { host: typeof config.host === "string" ? config.host : "", port: typeof config.port === "number" ? String(config.port) : typeof config.port === "string" ? config.port : "22", username: typeof config.username === "string" ? config.username : "", remoteWorkspacePath: typeof config.remoteWorkspacePath === "string" ? config.remoteWorkspacePath : "", privateKey: "", privateKeySecretId: config.privateKeySecretRef && typeof config.privateKeySecretRef === "object" && !Array.isArray(config.privateKeySecretRef) && typeof (config.privateKeySecretRef as { secretId?: unknown }).secretId === "string" ? String((config.privateKeySecretRef as { secretId: string }).secretId) : "", knownHosts: typeof config.knownHosts === "string" ? config.knownHosts : "", strictHostKeyChecking: typeof config.strictHostKeyChecking === "boolean" ? config.strictHostKeyChecking : true, }; } function readSandboxConfig(environment: Environment) { const config = environment.config ?? {}; return { provider: typeof config.provider === "string" && config.provider.trim().length > 0 ? config.provider : "fake", image: typeof config.image === "string" && config.image.trim().length > 0 ? config.image : "ubuntu:24.04", template: typeof config.template === "string" && config.template.trim().length > 0 ? config.template : "base", apiKey: "", apiKeySecretId: config.apiKeySecretRef && typeof config.apiKeySecretRef === "object" && !Array.isArray(config.apiKeySecretRef) && typeof (config.apiKeySecretRef as { secretId?: unknown }).secretId === "string" ? String((config.apiKeySecretRef as { secretId: string }).secretId) : "", timeoutMs: typeof config.timeoutMs === "number" ? String(config.timeoutMs) : typeof config.timeoutMs === "string" && config.timeoutMs.trim().length > 0 ? config.timeoutMs : "300000", reuseLease: typeof config.reuseLease === "boolean" ? config.reuseLease : false, }; } function SupportMark({ supported }: { supported: boolean }) { return supported ? ( Yes ) : ( No ); } export function CompanySettings() { const { companies, selectedCompany, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); // General settings local state const [companyName, setCompanyName] = useState(""); const [description, setDescription] = useState(""); const [brandColor, setBrandColor] = useState(""); const [logoUrl, setLogoUrl] = useState(""); const [logoUploadError, setLogoUploadError] = useState(null); const [editingEnvironmentId, setEditingEnvironmentId] = useState(null); const [environmentForm, setEnvironmentForm] = useState(createEmptyEnvironmentForm); const [probeResults, setProbeResults] = useState>({}); // Sync local state from selected company useEffect(() => { if (!selectedCompany) return; setCompanyName(selectedCompany.name); setDescription(selectedCompany.description ?? ""); setBrandColor(selectedCompany.brandColor ?? ""); setLogoUrl(selectedCompany.logoUrl ?? ""); }, [selectedCompany]); const [inviteError, setInviteError] = useState(null); const [inviteSnippet, setInviteSnippet] = useState(null); const [snippetCopied, setSnippetCopied] = useState(false); const [snippetCopyDelightId, setSnippetCopyDelightId] = useState(0); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); const environmentsEnabled = experimentalSettings?.enableEnvironments === true; const { data: environments } = useQuery({ queryKey: selectedCompanyId ? queryKeys.environments.list(selectedCompanyId) : ["environments", "none"], queryFn: () => environmentsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId) && environmentsEnabled, }); const { data: environmentCapabilities } = useQuery({ queryKey: selectedCompanyId ? ["environment-capabilities", selectedCompanyId] : ["environment-capabilities", "none"], queryFn: () => environmentsApi.capabilities(selectedCompanyId!), enabled: Boolean(selectedCompanyId) && environmentsEnabled, }); const { data: secrets } = useQuery({ queryKey: selectedCompanyId ? ["company-secrets", selectedCompanyId] : ["company-secrets", "none"], queryFn: () => secretsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const generalDirty = !!selectedCompany && (companyName !== selectedCompany.name || description !== (selectedCompany.description ?? "") || brandColor !== (selectedCompany.brandColor ?? "")); 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 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" ); } }); 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); } }); const environmentMutation = useMutation({ mutationFn: async (form: EnvironmentFormState) => { const body = buildEnvironmentPayload(form); if (editingEnvironmentId) { return await environmentsApi.update(editingEnvironmentId, body); } return await environmentsApi.create(selectedCompanyId!, body); }, onSuccess: async (environment) => { await queryClient.invalidateQueries({ queryKey: queryKeys.environments.list(selectedCompanyId!), }); setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); pushToast({ title: editingEnvironmentId ? "Environment updated" : "Environment created", body: `${environment.name} is ready.`, tone: "success", }); }, onError: (error) => { pushToast({ title: "Failed to save environment", body: error instanceof Error ? error.message : "Environment save failed.", tone: "error", }); }, }); const environmentProbeMutation = useMutation({ mutationFn: async (environmentId: string) => await environmentsApi.probe(environmentId), onSuccess: (probe, environmentId) => { setProbeResults((current) => ({ ...current, [environmentId]: probe, })); pushToast({ title: probe.ok ? "Environment probe passed" : "Environment probe failed", body: probe.summary, tone: probe.ok ? "success" : "error", }); }, onError: (error, environmentId) => { const failedEnvironment = (environments ?? []).find((environment) => environment.id === environmentId); setProbeResults((current) => ({ ...current, [environmentId]: { ok: false, driver: failedEnvironment?.driver ?? "local", summary: error instanceof Error ? error.message : "Environment probe failed.", details: null, }, })); pushToast({ title: "Environment probe failed", body: error instanceof Error ? error.message : "Environment probe failed.", tone: "error", }); }, }); const draftEnvironmentProbeMutation = useMutation({ mutationFn: async (form: EnvironmentFormState) => { const body = buildEnvironmentPayload(form); return await environmentsApi.probeConfig(selectedCompanyId!, body); }, onSuccess: (probe) => { pushToast({ title: probe.ok ? "Draft probe passed" : "Draft probe failed", body: probe.summary, tone: probe.ok ? "success" : "error", }); }, onError: (error) => { pushToast({ title: "Draft probe failed", body: error instanceof Error ? error.message : "Environment probe failed.", tone: "error", }); }, }); 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(); } useEffect(() => { setInviteError(null); setInviteSnippet(null); setSnippetCopied(false); setSnippetCopyDelightId(0); setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); setProbeResults({}); }, [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 }); } function handleEditEnvironment(environment: Environment) { setEditingEnvironmentId(environment.id); if (environment.driver === "ssh") { const ssh = readSshConfig(environment); setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "ssh", sshHost: ssh.host, sshPort: ssh.port, sshUsername: ssh.username, sshRemoteWorkspacePath: ssh.remoteWorkspacePath, sshPrivateKey: ssh.privateKey, sshPrivateKeySecretId: ssh.privateKeySecretId, sshKnownHosts: ssh.knownHosts, sshStrictHostKeyChecking: ssh.strictHostKeyChecking, }); return; } if (environment.driver === "sandbox") { const sandbox = readSandboxConfig(environment); setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "sandbox", sandboxProvider: sandbox.provider, sandboxImage: sandbox.image, sandboxTemplate: sandbox.template, sandboxApiKey: sandbox.apiKey, sandboxApiKeySecretId: sandbox.apiKeySecretId, sandboxTimeoutMs: sandbox.timeoutMs, sandboxReuseLease: sandbox.reuseLease, }); return; } setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "local", }); } function handleCancelEnvironmentEdit() { setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); } const discoveredPluginSandboxProviders = Object.entries(environmentCapabilities?.sandboxProviders ?? {}) .filter(([provider, capability]) => provider !== "fake" && capability.supportsRunExecution) .map(([provider, capability]) => ({ provider, displayName: capability.displayName || provider, })) .sort((left, right) => left.displayName.localeCompare(right.displayName)); const sandboxCreationEnabled = discoveredPluginSandboxProviders.length > 0; const sandboxSupportVisible = sandboxCreationEnabled; const pluginSandboxProviders = environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake" && !discoveredPluginSandboxProviders.some((provider) => provider.provider === environmentForm.sandboxProvider) ? [ ...discoveredPluginSandboxProviders, { provider: environmentForm.sandboxProvider, displayName: environmentForm.sandboxProvider }, ] : discoveredPluginSandboxProviders; useEffect(() => { if (environmentForm.driver !== "sandbox") return; if (environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake") return; const firstProvider = discoveredPluginSandboxProviders[0]?.provider; if (!firstProvider) return; setEnvironmentForm((current) => ( current.driver !== "sandbox" || (current.sandboxProvider.trim().length > 0 && current.sandboxProvider !== "fake") ? current : { ...current, sandboxProvider: firstProvider, } )); }, [discoveredPluginSandboxProviders, environmentForm.driver, environmentForm.sandboxProvider]); const environmentFormValid = environmentForm.name.trim().length > 0 && (environmentForm.driver !== "ssh" || ( environmentForm.sshHost.trim().length > 0 && environmentForm.sshUsername.trim().length > 0 && environmentForm.sshRemoteWorkspacePath.trim().length > 0 )) && (environmentForm.driver !== "sandbox" || environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake" && environmentForm.sandboxImage.trim().length > 0 && environmentForm.sandboxTimeoutMs.trim().length > 0 && Number.isFinite(Number(environmentForm.sandboxTimeoutMs)) && Number(environmentForm.sandboxTimeoutMs) > 0); return (

Company Settings

{/* General */}
General
setCompanyName(e.target.value)} /> 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... )}
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"} )}
)} {environmentsEnabled ? (
Environments
Environment choices use the same adapter support matrix as agent defaults. SSH is always available for remote-managed adapters, and sandbox environments appear only when a run-capable sandbox provider plugin is installed.
{sandboxSupportVisible ? ( ) : null} {(environmentCapabilities?.adapters.map((support) => ({ adapterType: support.adapterType, support, })) ?? ENVIRONMENT_SUPPORT_ROWS).map(({ adapterType, support }) => ( {sandboxSupportVisible ? ( ) : null} ))}
Environment support by adapter
Adapter Local SSHSandbox
{adapterLabels[adapterType] ?? adapterType} support.sandboxProviders[provider.provider] === "supported")} />
{(environments ?? []).length === 0 ? (
No environments saved for this company yet.
) : ( (environments ?? []).map((environment) => { const probe = probeResults[environment.id] ?? null; const isEditing = editingEnvironmentId === environment.id; return (
{environment.name} · {environment.driver}
{environment.description ? (
{environment.description}
) : null} {environment.driver === "ssh" ? (
{typeof environment.config.host === "string" ? environment.config.host : "SSH host"} ·{" "} {typeof environment.config.username === "string" ? environment.config.username : "user"}
) : environment.driver === "sandbox" ? (
{String(environment.config.provider ?? "fake")} sandbox provider ·{" "} {typeof environment.config.image === "string" ? environment.config.image : "ubuntu:24.04"}
) : (
Runs on this Paperclip host.
)}
{environment.driver !== "local" ? ( ) : null}
{probe ? (
{probe.summary}
{probe.details?.error && typeof probe.details.error === "string" ? (
{probe.details.error}
) : null}
) : null}
); }) )}
{editingEnvironmentId ? "Edit environment" : "Add environment"}
setEnvironmentForm((current) => ({ ...current, name: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, description: e.target.value }))} /> {environmentForm.driver === "ssh" ? (
setEnvironmentForm((current) => ({ ...current, sshHost: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshPort: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshUsername: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshRemoteWorkspacePath: e.target.value }))} />