import { useFormatters, useT } from "@agent-native/core/client/i18n"; import { disconnectManagedIntegrationInstallation, listManagedIntegrationBudgets, listManagedIntegrationInstallations, listManagedIntegrationScopes, listIntegrationEnvStatuses, listIntegrationStatuses, managedIntegrationOAuthUrl, managedSlackAgentManifestUrl, saveIntegrationEnvVars, saveManagedIntegrationBudget, saveManagedIntegrationScope, setIntegrationEnabled, setupIntegration, testManagedIntegrationInstallation, type ClientIntegrationInstallation, type ClientIntegrationScope, type ClientIntegrationUsageBudget, type ClientIntegrationStatus, type IntegrationEnvStatus, } from "@agent-native/core/client/integrations"; import { listBuiltInChannelIntegrations, type IntegrationCatalogEntry, type IntegrationCredentialRequirement, } from "@agent-native/core/integrations"; import { IconBrandDiscord, IconBrandSlack, IconBrandTelegram, IconBrandTeams, IconBrandWhatsapp, IconCheck, IconChevronRight, IconCopy, IconExternalLink, IconFileDescription, IconInfoCircle, IconLoader2, IconMail, IconPlug, } from "@tabler/icons-react"; import { type ReactNode, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "./ui/accordion"; import { Button } from "./ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "./ui/collapsible"; import { Input } from "./ui/input"; import { Skeleton } from "./ui/skeleton"; import { Switch } from "./ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; const CHANNELS = listBuiltInChannelIntegrations(); const PLATFORM_ICONS: Partial> = { slack: IconBrandSlack, "microsoft-teams": IconBrandTeams, discord: IconBrandDiscord, telegram: IconBrandTelegram, whatsapp: IconBrandWhatsapp, email: IconMail, }; function hasMissingRequiredCredentials( credentials: readonly IntegrationCredentialRequirement[], envStatusByKey: Map, ) { const alternatives = new Map< string, readonly IntegrationCredentialRequirement[] >(); for (const credential of credentials) { if (!credential.required) continue; if (!credential.alternativeGroup) { if (!envStatusByKey.get(credential.key)?.configured) return true; continue; } const group = alternatives.get(credential.alternativeGroup) ?? []; alternatives.set(credential.alternativeGroup, [...group, credential]); } return [...alternatives.values()].some((group) => group.every( (credential) => !envStatusByKey.get(credential.key)?.configured, ), ); } function HelpTooltip({ content }: { content: string }) { return ( {content} ); } function StatusPill({ tone, label, }: { tone: "neutral" | "success" | "warning"; label: string; }) { const toneClass = tone === "success" ? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : tone === "warning" ? "bg-amber-500/10 text-amber-700 dark:text-amber-300" : "bg-muted text-muted-foreground"; return ( {label} ); } function DisclosureSection({ title, summary, children, defaultOpen = false, }: { title: string; summary?: string; children: ReactNode; defaultOpen?: boolean; }) { return ( {title} {summary ? ( {summary} ) : null} {children} ); } /** Render a non-secret env value (e.g. EMAIL_AGENT_ADDRESS) as a copyable * text block. We can't read the actual value from the backend (env-status * only reports `configured: true|false`), so we offer a one-click reveal * that hits a server endpoint, falling back to "saved" if the value is * not exposed. For now we just render a "Saved — re-enter to change" * placeholder; a future endpoint can return the actual value. */ function PublicValueReveal({ envKey: _envKey }: { envKey: string }) { return (
Saved. Re-enter below to change.
); } function ConnectionStatus({ configured, enabled, }: { configured: boolean; enabled: boolean; }) { if (enabled) { return ; } if (configured) { return ; } return ; } export function MessagingSetupPanel() { const t = useT(); const { formatDate } = useFormatters(); const [statuses, setStatuses] = useState([]); const [loading, setLoading] = useState(true); const [envStatuses, setEnvStatuses] = useState([]); const [envLoading, setEnvLoading] = useState(true); const [envValues, setEnvValues] = useState>({}); const [savingKeysFor, setSavingKeysFor] = useState(null); const [togglingPlatform, setTogglingPlatform] = useState(null); const [setupPlatform, setSetupPlatform] = useState(null); const [copiedWebhook, setCopiedWebhook] = useState(null); const [installations, setInstallations] = useState< ClientIntegrationInstallation[] >([]); const [installationAction, setInstallationAction] = useState( null, ); const [scopes, setScopes] = useState([]); const [budgets, setBudgets] = useState([]); const [scopeBudget, setScopeBudget] = useState>({}); const [savingScope, setSavingScope] = useState(null); const refreshStatuses = async () => { setLoading(true); try { setStatuses(await listIntegrationStatuses()); } finally { setLoading(false); } }; useEffect(() => { let active = true; listIntegrationStatuses() .then((rows) => { if (active) { setStatuses(rows); setLoading(false); } }) .catch(() => { if (active) setLoading(false); }); return () => { active = false; }; }, []); useEffect(() => { let active = true; listManagedIntegrationInstallations("slack") .then((rows) => { if (active) setInstallations(rows); }) .catch(() => {}); listManagedIntegrationScopes("slack") .then((rows) => { if (active) setScopes(rows); }) .catch(() => {}); listManagedIntegrationBudgets() .then((rows) => { if (active) setBudgets(rows); }) .catch(() => {}); return () => { active = false; }; }, []); useEffect(() => { let active = true; listIntegrationEnvStatuses() .then((rows) => { if (active) { setEnvStatuses(rows); setEnvLoading(false); } }) .catch(() => { if (active) setEnvLoading(false); }); return () => { active = false; }; }, []); const envStatusByKey = useMemo( () => new Map(envStatuses.map((status) => [status.key, status])), [envStatuses], ); const statusByPlatform = useMemo( () => new Map(statuses.map((status) => [status.platform, status])), [statuses], ); const refreshEnvStatus = async () => { setEnvLoading(true); try { setEnvStatuses(await listIntegrationEnvStatuses()); } finally { setEnvLoading(false); } }; const saveEnvKeys = async ( platform: IntegrationCatalogEntry, keys: string[], ) => { const vars = keys .map((key) => ({ key, value: envValues[key]?.trim() || "" })) .filter((item) => item.value); if (vars.length === 0) { toast.error("Add the required credentials first."); return; } setSavingKeysFor(platform.id); try { await saveIntegrationEnvVars(vars); toast.success(`${platform.name} credentials saved`); setEnvValues((current) => { const next = { ...current }; for (const key of keys) delete next[key]; return next; }); await refreshEnvStatus(); await refreshStatuses(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to save credentials", ); } finally { setSavingKeysFor(null); } }; const togglePlatform = async ( platform: IntegrationCatalogEntry, enabled: boolean, ) => { setTogglingPlatform(platform.id); try { await setIntegrationEnabled(platform.id, !enabled); toast.success( enabled ? `${platform.name} disconnected` : `${platform.name} connected`, ); await refreshStatuses(); } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to update integration", ); } finally { setTogglingPlatform(null); } }; const runSetup = async (platform: IntegrationCatalogEntry) => { setSetupPlatform(platform.id); try { await setupIntegration(platform.id); toast.success( platform.id === "telegram" ? "Telegram webhook registered" : `${platform.name} setup complete`, ); await refreshStatuses(); } catch (error) { toast.error( error instanceof Error ? error.message : `Failed to set up ${platform.name}`, ); } finally { setSetupPlatform(null); } }; const copyWebhook = async (webhookUrl: string) => { await navigator.clipboard.writeText(webhookUrl); setCopiedWebhook(webhookUrl); toast.success("Webhook URL copied"); setTimeout(() => setCopiedWebhook(null), 1500); }; const runInstallationAction = async ( installation: ClientIntegrationInstallation, action: "test" | "disconnect", ) => { setInstallationAction(`${action}:${installation.id}`); try { await (action === "test" ? testManagedIntegrationInstallation(installation.id) : disconnectManagedIntegrationInstallation(installation.id)); setInstallations(await listManagedIntegrationInstallations("slack")); toast.success( action === "test" ? t("messaging.managed.connectionChecked") : t("messaging.managed.workspaceDisconnected"), ); } catch (error) { toast.error( error instanceof Error ? error.message : t("messaging.managed.actionFailed"), ); } finally { setInstallationAction(null); } }; const updateScopePolicy = async ( scope: ClientIntegrationScope, policy: Partial, ) => { setSavingScope(scope.id); try { await saveManagedIntegrationScope({ platform: scope.platform, tenantId: scope.tenantId, conversationId: scope.conversationId, conversationType: scope.conversationType, trust: scope.trust, orgId: scope.orgId, serviceOwnerEmail: scope.serviceOwnerEmail, defaultModel: scope.defaultModel, policy: { ...scope.policy, ...policy }, }); setScopes(await listManagedIntegrationScopes("slack")); toast.success(t("messaging.managed.channelPolicyUpdated")); } catch (error) { toast.error( error instanceof Error ? error.message : t("messaging.managed.policyUpdateFailed"), ); } finally { setSavingScope(null); } }; const saveScopeBudget = async (scope: ClientIntegrationScope) => { const dollars = Number(scopeBudget[scope.id]); if (!Number.isFinite(dollars) || dollars <= 0) { toast.error(t("messaging.managed.positiveMonthlyBudget")); return; } setSavingScope(`budget:${scope.id}`); try { await saveManagedIntegrationBudget({ subject: { type: "scope", scope: { platform: scope.platform, tenantId: scope.tenantId, conversationId: scope.conversationId, }, }, period: "month", limitMicros: Math.round(dollars * 1_000_000), }); setBudgets(await listManagedIntegrationBudgets()); toast.success(t("messaging.managed.channelBudgetSaved")); } catch (error) { toast.error( error instanceof Error ? error.message : t("messaging.managed.budgetSaveFailed"), ); } finally { setSavingScope(null); } }; return (
{CHANNELS.map((platform) => { const status = statusByPlatform.get(platform.id); const configured = !!status?.configured || (platform.id === "slack" && installations.some( (installation) => installation.status === "connected", )); const enabled = !!status?.enabled; const envKeys = platform.credentialRequirements; const primaryEnvKeys = envKeys.filter( (envKey) => envKey.key !== "SLACK_BOT_TOKEN", ); const legacyEnvKeys = envKeys.filter( (envKey) => envKey.key === "SLACK_BOT_TOKEN", ); const missingRequiredCredentials = hasMissingRequiredCredentials( envKeys, envStatusByKey, ); const configuredCredentialCount = envKeys.filter( (envKey) => envStatusByKey.get(envKey.key)?.configured, ).length; const credentialSummary = envLoading ? "Checking saved credentials" : missingRequiredCredentials ? "Required credentials are missing" : `${configuredCredentialCount} saved`; const Icon = PLATFORM_ICONS[platform.iconKey] ?? IconPlug; return (

{platform.name}

{platform.description}

{enabled ? "Messages are flowing" : configured ? "Ready to connect" : "Finish setup to connect"}

{envLoading ? ( ) : ( credentialSummary )}

{platform.id === "telegram" && configured ? ( ) : null} {!configured && !enabled ? ( Save the required credentials first. ) : ( )}
{platform.id === "slack" ? (

{t("messaging.managed.description")}{" "} {t("messaging.managed.agentManifestDescription")}

{missingRequiredCredentials ? ( ) : ( )}
{missingRequiredCredentials ? (

{t("messaging.managed.requiredCredentials")}

) : null} {installations.length ? (
{installations.map((installation) => (
{installation.teamName || installation.enterpriseName || installation.teamId || t("messaging.managed.workspaceFallback")}
{t("messaging.managed.scopesUpdated", { count: installation.scopes.length, date: formatDate(installation.updatedAt, { dateStyle: "medium", }), })}
))}
) : (

{t("messaging.managed.empty")}

)}
) : null} {platform.id === "slack" && scopes.length ? (

{t("messaging.managed.channelAccessDescription")}

{scopes.map((scope) => { const subjectId = JSON.stringify([ scope.platform, scope.tenantId, scope.conversationId, ]); const budget = budgets.find( (item) => item.subjectType === "scope" && item.subjectId === subjectId && item.period === "month", ); return ( {scope.conversationId} {t("messaging.managed.isolatedIdentity", { trust: t( `messaging.managed.trust.${scope.trust}`, ), })}
{[ [ "requireMention", t("messaging.managed.requireMention"), ], [ "allowGuests", t("messaging.managed.allowGuests"), ], [ "allowExternalShared", t("messaging.managed.allowSlackConnect"), ], ].map(([key, label]) => ( ))}
setScopeBudget((current) => ({ ...current, [scope.id]: event.target.value, })) } placeholder="25" />
); })}
) : null}
    {platform.setup.steps.map((step, index) => (
  1. {index + 1}. {step}
  2. ))}
{primaryEnvKeys.map((envKey) => { const envStatus = envStatusByKey.get(envKey.key); const isConfigured = !!envStatus?.configured; const helpText = envKey.helpText ?? envStatus?.helpText; const label = envKey.label || envStatus?.label || envKey.key; // Email agent address is not a secret — show it plainly // so users can copy and share it. const isPublicValue = envKey.key === "EMAIL_AGENT_ADDRESS"; return (
{helpText ? ( ) : null}
{isConfigured ? ( ) : ( )}
{isConfigured && isPublicValue ? ( ) : !isConfigured ? ( setEnvValues((current) => ({ ...current, [envKey.key]: event.target.value, })) } placeholder={ isPublicValue ? "agent@yourcompany.com" : `Enter ${label}` } autoComplete="off" /> ) : null}
); })}
{legacyEnvKeys.length ? ( {legacyEnvKeys[0]?.label}

{legacyEnvKeys[0]?.helpText}

{legacyEnvKeys.map((envKey) => { const envStatus = envStatusByKey.get(envKey.key); const isConfigured = !!envStatus?.configured; const helpText = envKey.helpText ?? envStatus?.helpText; const label = envKey.label || envStatus?.label || envKey.key; return (
{helpText ? ( ) : null}
{!isConfigured ? ( setEnvValues((current) => ({ ...current, [envKey.key]: event.target.value, })) } placeholder={`Enter ${label}`} autoComplete="off" /> ) : null}
); })} {legacyEnvKeys.some( (envKey) => !envStatusByKey.get(envKey.key)?.configured, ) ? ( ) : null}
) : null} {missingRequiredCredentials ? ( ) : null}
{status?.webhookUrl ? (
{status.webhookUrl}
) : null}
{platform.documentation.externalHref ? ( ) : null}
); })}
{loading ? (
) : null}
); }