import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Text, TextInput, View } from 'react-native'; import { Search } from 'lucide-react-native'; import { ConnectedConnectorCard, ConnectorCard } from './ConnectorCards'; import { ConnectorDetailView } from './ConnectorDetailView'; import { SUPERAGENT_CONNECTOR_CATALOG } from './connectorCatalog'; import { sortConnectorsByPopularity } from './connectorCatalogUtils'; import { getConnectorScopesForMode } from './connectorPanelUtils'; import { connectorPanelStyles as cStyles } from './connectorPanelStyles'; import { editorShellStyles } from '../editor/editorShellStyles'; import { useAgentBi } from '../../analytics/mixpanelContext'; import { useSuperagentConnectors } from '../../runtime/runtimeContext'; import { themedColor } from '../../theme'; import type { SuperagentAgent, SuperagentConnector, SuperagentConnectorAccessMode } from '../../types'; export function ConnectorsPanel({ agent, isVisible = true, onBackHandlerChange, }: { agent: SuperagentAgent; // Whether the containing settings sheet is open. When it closes we reset the // in-sheet navigation so reopening always starts at the main list (and the // top-bar shows ✕, not a stale ←). isVisible?: boolean; // Report a back handler so the modal top-bar swaps its ✕ for a ← while // viewing a connector's detail (null = at the main view). onBackHandlerChange?: (back: (() => void) | null) => void; }) { const { availableConnectors, connectedConnectors, connectingConnectorId, isLoadingConnectors: isLoading, onCancelConnectorConnection: onCancel, onConnectConnector: onConnect, onDisconnectConnector: onDisconnect, onRemoveConnector: onRemove, } = useSuperagentConnectors(); const bi = useAgentBi(); // In-sheet navigation: tapping a connector pushes its detail view within the // same settings modal (no nested modal-on-modal). const [detailConnector, setDetailConnector] = useState(null); const [searchQuery, setSearchQuery] = useState(''); // Native-only: report the settled query, not per keystroke. Web has fixed filter // tabs (Plugins Filter) instead of a free-text box, so this is its own event. useEffect(() => { const normalizedQuery = searchQuery.trim(); if (!normalizedQuery) return; const timer = setTimeout(() => { void bi.trackEditor('Connector Search', { query_length: normalizedQuery.length }); }, 400); return () => clearTimeout(timer); }, [searchQuery, bi]); useEffect(() => { onBackHandlerChange?.(detailConnector ? () => setDetailConnector(null) : null); }, [detailConnector, onBackHandlerChange]); useEffect(() => () => onBackHandlerChange?.(null), [onBackHandlerChange]); // Reset to the main list whenever the sheet is hidden, so reopening never // resurfaces a stale detail screen with a broken back control. useEffect(() => { if (!isVisible) { setDetailConnector(null); } }, [isVisible]); // Once the in-flight connection for the connector we're viewing ends (success, // failure, or cancel), pop back to the list so it lands under "Apps connected". const prevConnectingId = useRef(connectingConnectorId); useEffect(() => { const previous = prevConnectingId.current; prevConnectingId.current = connectingConnectorId; if (detailConnector && previous === detailConnector.id && connectingConnectorId !== detailConnector.id) { setDetailConnector(null); } }, [connectingConnectorId, detailConnector]); const connectedIds = useMemo( () => new Set((connectedConnectors ?? []).map((connector) => connector.id)), [connectedConnectors], ); // Not-yet-connected connectors, most popular first (mirrors the web ranking), // narrowed by the search query when present. const available = useMemo(() => { const query = searchQuery.trim().toLowerCase(); const notConnected = (availableConnectors ?? SUPERAGENT_CONNECTOR_CATALOG).filter( (connector) => !connectedIds.has(connector.id), ); const matched = query ? notConnected.filter((connector) => [connector.id, connector.name, connector.subtitle, connector.description, connector.category] .filter(Boolean) .join(' ') .toLowerCase() .includes(query), ) : notConnected; return sortConnectorsByPopularity(matched); }, [availableConnectors, connectedIds, searchQuery]); const connect = async (connector: SuperagentConnector, accessMode: SuperagentConnectorAccessMode | null) => { const result = await onConnect?.({ accessMode: accessMode ?? undefined, agentId: agent.id, connectorId: connector.id, scopes: getConnectorScopesForMode(connector, accessMode ?? 'full_access'), }); if (result) { void bi.trackEditor('Plugin Connect', { connector_type: connector.id, access_mode: accessMode ?? 'full_access' }); } // Stay in the detail view so the spinner/Cancel remain visible; the effect // above pops back once the connection flow resolves. }; if (isLoading) { return ( Loading connectors... ); } if (detailConnector) { return ( onCancel?.({ agentId: agent.id, connectorId: detailConnector.id })} onConnect={connect} /> ); } const connected = connectedConnectors ?? []; return ( Apps connected {connected.length > 0 ? {connected.length} : null} {connected.length > 0 ? ( connected.map((connector) => ( )) ) : ( No apps connected yet. )} All connectors {available.length > 0 ? ( available.map((connector) => ( setDetailConnector(connector)} /> )) ) : ( No matching connectors. )} ); }