import React, { useState } from 'react'; import { ActivityIndicator, Image, Pressable, Text, View } from 'react-native'; import { AlertCircle, Info, MoreHorizontal, Plug, Unplug } from 'lucide-react-native'; import { ConnectorBrandIcon, hasConnectorBrandIcon } from './connectorBrandIcons'; import { connectorDescription, connectorSubtitle, getConnectorScopesForMode } from './connectorPanelUtils'; import { connectorPanelStyles as cStyles } from './connectorPanelStyles'; import { editorShellStyles } from '../editor/editorShellStyles'; import { useAgentBi } from '../../analytics/mixpanelContext'; import { styles as sharedStyles } from '../../styles'; import { themedColor } from '../../theme'; import type { SuperagentConnector, SuperagentConnectorAccessMode, SuperagentConnectorActionInput, } from '../../types'; export function ConnectorCard({ agentId, connector, isConnecting, onCancel, onSelect, }: { agentId: string; connector: SuperagentConnector; isConnecting?: boolean; onCancel?: (input: { agentId: string; connectorId: string }) => void; onSelect: () => void; }) { // While this connector is connecting, the card stays a spinner + Cancel (not a // pressable "Connect" chip) so backing out of the detail view during OAuth // still leaves the flow visible and cancelable. if (isConnecting) { return ( {connector.name} {connectorSubtitle(connector)} onCancel?.({ agentId, connectorId: connector.id })} style={({ pressed }) => [editorShellStyles.cancelConnectButton, pressed && sharedStyles.pressed]} > Cancel ); } return ( [editorShellStyles.connectorCard, cStyles.cardSurface, pressed && sharedStyles.pressed]} > {connector.name} {connectorSubtitle(connector)} Connect ); } export function ConnectedConnectorCard({ agentId, connector, isConnecting, onCancel, onConnect, onDisconnect, onRemove, }: { agentId: string; connector: SuperagentConnector; isConnecting: boolean; onCancel?: (input: { agentId: string; connectorId: string }) => void; onConnect?: (input: SuperagentConnectorActionInput) => Promise | boolean | string | void; onDisconnect?: (input: SuperagentConnectorActionInput) => Promise | void; onRemove?: (input: SuperagentConnectorActionInput) => Promise | void; }) { const status = connector.status ?? 'active'; const [menuOpen, setMenuOpen] = useState(false); const bi = useAgentBi(); const hasAccessModes = !!connector.accessModes; const currentMode = connector.accessMode ?? null; const oppositeMode: SuperagentConnectorAccessMode = currentMode === 'read_only' ? 'full_access' : 'read_only'; return ( {connector.name} {connector.accountIdentifier || connectorDescription(connector)} {isConnecting ? ( onCancel?.({ agentId, connectorId: connector.id })} style={({ pressed }) => [editorShellStyles.cancelConnectButton, pressed && sharedStyles.pressed]} > Cancel ) : ( setMenuOpen((open) => !open)} style={({ pressed }) => [editorShellStyles.moreButton, pressed && sharedStyles.pressed]} > )} {menuOpen ? ( {hasAccessModes && onConnect && !connector.requiresConnectionConfig ? ( { setMenuOpen(false); const result = await onConnect({ accessMode: oppositeMode, agentId, connectorId: connector.id, forceReconnect: true, scopes: getConnectorScopesForMode(connector, oppositeMode), }); if (result) { void bi.trackEditor('Plugin Access Mode Switch', { connector_type: connector.id, from_mode: currentMode ?? 'full_access', to_mode: oppositeMode }); } }} /> ) : null} {connector.requiresConnectionConfig ? ( ) : ( { setMenuOpen(false); const result = await onConnect?.({ accessMode: connector.accessMode ?? undefined, agentId, connectorId: connector.id, forceReconnect: true, scopes: connector.scopes?.length ? connector.scopes : getConnectorScopesForMode(connector, connector.accessMode ?? 'full_access'), }); // A successful reconnect / switch-account counts as a connect too. if (result) { void bi.trackEditor('Plugin Connect', { connector_type: connector.id, access_mode: connector.accessMode ?? 'full_access' }); } }} /> )} {status !== 'disconnected' ? ( { setMenuOpen(false); onDisconnect?.({ agentId, connectorId: connector.id }); }} /> ) : null} { setMenuOpen(false); onRemove?.({ agentId, connectorId: connector.id }); }} /> ) : null} ); } export function ConnectorIcon({ connector }: { connector: SuperagentConnector }) { const iconBackgroundColor = connector.iconBackgroundColor ?? '#FFFFFF'; const hasBrandIcon = hasConnectorBrandIcon(connector.id); const [imageFailed, setImageFailed] = useState(false); const iconUrl = connector.iconUrl?.trim(); return ( {hasBrandIcon ? ( ) : iconUrl && !imageFailed ? ( setImageFailed(true)} resizeMode="contain" source={{ uri: iconUrl }} style={editorShellStyles.connectorIconImage} /> ) : ( )} ); } // Matches the web ConnectorStatusLabel: a small colored dot + label, no filled // pill. Active = green dot + green text; expired = alert + danger; disconnected // = unplug + muted. export function StatusBadge({ status }: { status: SuperagentConnector['status'] }) { const isActive = status === 'active' || !status; const isDisconnected = status === 'disconnected'; if (isActive) { return ( Active ); } if (isDisconnected) { return ( Disconnected ); } return ( {status} ); } function MenuAction({ destructive, label, onPress }: { destructive?: boolean; label: string; onPress: () => void }) { return ( [ editorShellStyles.menuAction, pressed && sharedStyles.pressed, ]}> {label} ); } function MenuNote({ label }: { label: string }) { return ( {label} ); }