import { IconPlus, IconBrandSlack, IconBrandTelegram, IconBrandWhatsapp, IconBrandGoogleDrive, IconTerminal2, IconBuildingSkyscraper, IconCopy, IconCheck, IconChevronLeft, IconExternalLink, IconCircleCheck, IconInfoCircle, } from "@tabler/icons-react"; import React, { useState, useCallback, useEffect } from "react"; import { agentNativePath } from "../api-path.js"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { useIntegrationStatus, type IntegrationStatus, } from "./useIntegrationStatus.js"; import { isNonPublicWebhookUrl } from "./webhook-url.js"; // ─── Platform config ───────────────────────────────────────────────────────── interface PlatformInfo { id: string; label: string; icon: React.ComponentType; description: string; envVars: string[]; setupSteps: string[]; docsUrl?: string; /** If true, this is a "client" integration (user connects TO the agent) rather than a webhook */ isClient?: boolean; } const PLATFORMS: PlatformInfo[] = [ { id: "slack", label: "Slack", icon: IconBrandSlack, description: "@mention the agent in a Slack thread or DM it, and it replies in that thread.", envVars: ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], setupSteps: [ "At api.slack.com/apps, create an app for your workspace, then under OAuth & Permissions add the bot scopes app_mentions:read, chat:write, channels:history, and im:history", "Click Install to Workspace, then copy the Bot User OAuth Token and the Signing Secret (Basic Information → App Credentials) into the two secrets listed below", "Under Event Subscriptions, turn events on, paste the webhook URL below as the Request URL, and subscribe to the bot events app_mention and message.im", "Invite the bot to a channel, @mention it in a thread, and confirm it replies in that same thread", "Running inside a Dispatch workspace instead? Connect Slack from Settings → Messaging there — it stores workspace tokens for you and this page is not needed.", ], docsUrl: "https://api.slack.com/apps", }, { id: "telegram", label: "Telegram", icon: IconBrandTelegram, description: "Chat with your agent via a Telegram bot.", envVars: ["TELEGRAM_BOT_TOKEN"], setupSteps: [ "Message @BotFather on Telegram to create a new bot", "Copy the bot token into your environment", 'Click "Setup webhook" below to register automatically', ], }, { id: "whatsapp", label: "WhatsApp", icon: IconBrandWhatsapp, description: "Connect your agent to WhatsApp Business.", envVars: ["WHATSAPP_TOKEN", "WHATSAPP_VERIFY_TOKEN"], setupSteps: [ "Create a Meta Business app at developers.facebook.com", "Set up WhatsApp Business API", "Configure the webhook URL and verify token", "Copy the access token into your environment", ], docsUrl: "https://developers.facebook.com/docs/whatsapp", }, { id: "google-docs", label: "Google Docs", icon: IconBrandGoogleDrive, description: "Tag the agent in Google Doc comments to get responses.", envVars: ["GOOGLE_SERVICE_ACCOUNT_KEY"], setupSteps: [ "Create a Google Cloud service account and download the JSON key", "Set GOOGLE_SERVICE_ACCOUNT_KEY in your environment (JSON string or file path)", "Share your Google Docs with the service account email", 'Write a comment containing "@Agent" to trigger the agent', ], }, { id: "openclaw", label: "OpenClaw", icon: IconTerminal2, description: "Access this agent from OpenClaw's unified agent interface.", envVars: [], isClient: true, setupSteps: [ "Install OpenClaw: npm install -g openclaw", "Add this agent's URL as a provider in your OpenClaw config", "OpenClaw discovers your agent's capabilities via the A2A protocol", ], }, { id: "claude-code", label: "Claude Code", icon: IconTerminal2, description: "Let Claude Code call this agent via A2A for data and actions.", envVars: [], isClient: true, setupSteps: [ "Your agent exposes an A2A endpoint at /.well-known/agent-card.json", "In Claude Code, reference your agent's URL when asking for data", "Claude Code will discover and call your agent's skills automatically", ], }, { id: "builder", label: "Builder.io", icon: IconBuildingSkyscraper, description: "One chat interface that orchestrates all your agents together.", envVars: [], isClient: true, setupSteps: [ "Connect your agent-native apps in your Builder.io workspace", "Builder.io discovers each agent's skills via A2A", "Chat with one agent that can trigger actions across all your apps", ], docsUrl: "https://www.builder.io?utm_source=agent-native&utm_medium=product&utm_campaign=integrations&utm_content=integrations_panel", }, ]; function useAgentEngineConfigured() { const [configured, setConfigured] = useState(undefined); const refresh = useCallback(() => { fetch(agentNativePath("/_agent-native/agent-engine/status")) .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (typeof data?.configured === "boolean") { setConfigured(data.configured); } }) .catch(() => {}); }, []); useEffect(() => { refresh(); window.addEventListener("agent-engine:configured-changed", refresh); return () => window.removeEventListener("agent-engine:configured-changed", refresh); }, [refresh]); return configured; } // ─── Integration detail view ───────────────────────────────────────────────── function IntegrationDetail({ platform, serverStatus, onBack, onRefresh, }: { platform: PlatformInfo; serverStatus?: IntegrationStatus; onBack: () => void; onRefresh: () => void; }) { const t = useT(); const [toggling, setToggling] = useState(false); const [copied, setCopied] = useState(false); const [toggleError, setToggleError] = useState(null); const agentEngineConfigured = useAgentEngineConfigured(); const handleToggle = useCallback(async () => { setToggling(true); setToggleError(null); try { const action = serverStatus?.enabled ? "disable" : "enable"; const res = await fetch( agentNativePath(`/_agent-native/integrations/${platform.id}/${action}`), { method: "POST" }, ); if (res.ok) { onRefresh(); return; } // Surface the real reason instead of silently doing nothing. // The endpoint returns `{ error }` for known failures (admin gating, // missing secrets, etc.); fall back to status text otherwise. const data = (await res.json().catch(() => null)) as { error?: string; } | null; setToggleError( data?.error || res.statusText || `Couldn't ${action} ${platform.label} (HTTP ${res.status})`, ); } catch (err) { setToggleError( err instanceof Error ? err.message : t("integrations.networkError"), ); } finally { setToggling(false); } }, [platform.id, platform.label, serverStatus?.enabled, onRefresh]); const handleCopy = useCallback(async (text: string) => { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }, []); const handleOpenLlmSettings = useCallback(() => { window.dispatchEvent( new CustomEvent("agent-panel:open-settings", { detail: { section: "llm" }, }), ); }, []); const isConfigured = serverStatus?.configured ?? false; const isEnabled = serverStatus?.enabled ?? false; const showAgentEnginePrereq = !platform.isClient && agentEngineConfigured === false; const serviceAccountEmail = typeof serverStatus?.details?.serviceAccountEmail === "string" ? serverStatus.details.serviceAccountEmail : null; return (
{platform.label}
{platform.description}
{showAgentEnginePrereq && (
{t("integrations.agentEngineRequired")}

{t("integrations.agentEngineDescription", { platform: platform.label, })}

)} {/* Setup steps */}
{t("integrations.setup")}
    {platform.setupSteps.map((step, i) => (
  1. {i + 1}. {step}
  2. ))}
{serviceAccountEmail && (
{t("integrations.shareDocumentsWith")}
{serviceAccountEmail} {t("integrations.copyServiceAccountEmail")}
)} {/* Required secrets */} {platform.envVars.length > 0 && (
{t("integrations.requiredSecrets")}
{platform.envVars.map((v) => (
{v} {isConfigured && ( )}
))}
{!isConfigured && (

{t("integrations.envHelp")}

)}
)} {/* Webhook URL */} {serverStatus?.webhookUrl && !platform.isClient && (
{t("integrations.webhookUrl")}
{isNonPublicWebhookUrl(serverStatus.webhookUrl) ? (
{t("integrations.webhookUrlLocalOnly", { platform: platform.label, url: serverStatus.webhookUrl, })}
) : (
{serverStatus.webhookUrl} {t("integrations.copy")}
)}
)} {/* Docs link */} {platform.docsUrl && ( {t("integrations.documentation")} )} {/* Enable/disable for server integrations */} {serverStatus && !platform.isClient && isConfigured && ( )} {/* Status for client integrations */} {platform.isClient && (
{t("integrations.clientAvailable")}
)} {serverStatus?.error && (

{serverStatus.error}

)} {toggleError && (

{toggleError}

)}
); } // ─── Add integration picker ────────────────────────────────────────────────── function AddIntegrationPicker({ connectedIds, onSelect, }: { connectedIds: Set; onSelect: (platform: PlatformInfo) => void; }) { return (
{PLATFORMS.filter((p) => !connectedIds.has(p.id)).map((platform) => ( ))}
); } // ─── Main panel ────────────────────────────────────────────────────────────── export function IntegrationsPanel() { const t = useT(); const { statuses, loading, refetch } = useIntegrationStatus(); const [selectedPlatform, setSelectedPlatform] = useState( null, ); const [showPicker, setShowPicker] = useState(false); const statusMap = new Map(statuses.map((s) => [s.platform, s])); // Show connected (enabled or configured) integrations const connectedPlatforms = PLATFORMS.filter((p) => { const s = statusMap.get(p.id); return s?.configured || s?.enabled; }); const connectedIds = new Set(connectedPlatforms.map((p) => p.id)); if (selectedPlatform) { return ( setSelectedPlatform(null)} onRefresh={refetch} /> ); } if (showPicker) { return (
{t("integrations.addChatIntegration")}
{ setSelectedPlatform(p); setShowPicker(false); }} />
); } return (
{t("integrations.chatIntegrations")}
{t("integrations.chatIntegrationsDescription")}
{t("integrations.addIntegration")}
{loading ? (
) : connectedPlatforms.length === 0 ? (
{t("integrations.dispatchEntrypoint")}{" "} dispatch template .
) : (
{connectedPlatforms.map((platform) => { const s = statusMap.get(platform.id); return ( ); })}
{t("integrations.sharedMessaging")}
)}
); }