import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { themedColor } from '../../theme'; import { ActivityIndicator, Modal, Pressable, ScrollView, Text, View } from 'react-native'; import { AlertCircle, Archive, ChevronDown, ChevronLeft, Coins, Edit3, FileText, GitBranch, Play, RefreshCw, RotateCcw, Settings, Timer, ToggleLeft, ToggleRight, Trash2, Webhook, X, Zap, type LucideIcon, } from 'lucide-react-native'; import { AgentSettingsPanel } from '../settings/AgentSettingsPanel'; import { ChannelsPanel } from '../channels/ChannelsPanel'; import { ConnectorsPanel } from '../connectors/ConnectorsPanel'; import { SUPERAGENT_CONNECTOR_CATALOG } from '../connectors/connectorCatalog'; import { featureRefreshKeys, getEditorTabLabel } from '../settings/featureMenu'; import { editorShellStyles } from './editorShellStyles'; import { FilesPanel } from './FilesPanel'; import { SharingPanel } from '../sharing/SharingPanel'; import { useAgentBi } from '../../analytics/mixpanelContext'; import { useSuperagentAutomations, useSuperagentRuntimeContext, useSuperagentWorkflows } from '../../runtime/runtimeContext'; import { styles } from '../../styles'; import { describeWorkflowTrigger, formatWorkflowRuns, formatWorkflowStatus, isDeactivatedByFailures, isWorkflowActive, isWorkflowArchived, sortWorkflows, } from './workflowUtils'; import type { SuperagentAgent, SuperagentAutomation, SuperagentAutomationActionInput, SuperagentAutomationCreditsSummary, SuperagentEditorTab, SuperagentWorkflow, SuperagentWorkflowActionInput, } from '../../types'; const AUTOMATION_ICONS: Partial> = { connector: Webhook, entity: Zap, scheduled: Timer, }; export function EditorDrawer({ activeTab, agent, isVisible, onClose, }: { activeTab: SuperagentEditorTab; agent: SuperagentAgent; isVisible: boolean; onClose: () => void; }) { // The modal always shows exactly the one feature it was opened for; each panel // reads its own runtime slice from context, so the drawer only routes by tab. const effectiveTab = activeTab === 'chat' ? 'connectors' : activeTab; // Generic "refresh this modal's data" header action. Which domains a tab // reloads is owned by featureMenu (FEATURE_TABS[].refresh); we just resolve // each to its runtime handler + loading flag, so any refreshable tab gets the // button with nothing wired panel-side. const runtime = useSuperagentRuntimeContext(); const bi = useAgentBi(); const activeRefreshers = featureRefreshKeys(effectiveTab) .map((key): { refresh?: (agentId: string) => Promise | void; loading?: boolean } => { switch (key) { case 'channels': return { refresh: runtime.onRefreshChannels, loading: runtime.isLoadingChannels }; case 'files': return { refresh: runtime.onRefreshFiles, loading: runtime.isLoadingFiles }; case 'automations': return agent.workflowsEnabled ? { refresh: runtime.onRefreshWorkflows, loading: runtime.isLoadingWorkflows } : { refresh: runtime.onRefreshAutomations, loading: runtime.isLoadingAutomations }; case 'agentSettings': return { refresh: runtime.onRefreshAgentSettings, loading: runtime.isLoadingAgentSettings }; case 'collaborators': return { refresh: runtime.onRefreshCollaborators, loading: runtime.isLoadingCollaborators }; } }) .filter((entry): entry is { refresh: (agentId: string) => Promise | void; loading?: boolean } => !!entry.refresh); const onRefresh = activeRefreshers.length ? (agentId: string) => activeRefreshers.forEach((entry) => entry.refresh(agentId)) : undefined; const isRefreshing = activeRefreshers.some((entry) => entry.loading); // A panel with in-place sub-navigation (e.g. Connectors → connector detail) // registers a back handler here so the top-bar shows a ← that pops one level, // instead of the ✕ (which closes the whole sheet). Reset when the tab changes // or the sheet is hidden so reopening always starts at the panel's main view. const [panelBack, setPanelBack] = useState<(() => void) | null>(null); const registerPanelBack = useCallback((back: (() => void) | null) => setPanelBack(() => back), []); useEffect(() => { setPanelBack(null); }, [effectiveTab, isVisible]); return ( [ editorShellStyles.closeButton, pressed && styles.pressed, ]}> {panelBack ? ( ) : ( )} {getEditorTabLabel(effectiveTab)} {onRefresh ? ( { void bi.trackEditor('Feature Refresh', { feature: effectiveTab }); onRefresh(agent.id); }} style={({ pressed }) => [editorShellStyles.closeButton, pressed && styles.pressed]} > {isRefreshing ? ( ) : ( )} ) : null} {effectiveTab === 'connectors' ? ( ) : effectiveTab === 'automations' ? ( ) : effectiveTab === 'files' ? ( ) : effectiveTab === 'channels' ? ( ) : effectiveTab === 'agent_settings' ? ( ) : effectiveTab === 'security' ? ( ) : effectiveTab === 'sharing' ? ( ) : ( )} ); } function TasksPanel({ agent }: { agent: SuperagentAgent }) { // An agent runs EITHER the workflows pipeline OR legacy automations — never // both. Gate on the agent flag so a workflows-enabled agent shows its // workflows here instead of an always-empty automations list. return agent.workflowsEnabled ? : ; } function AutomationsPanel({ agent }: { agent: SuperagentAgent }) { const { automations = [], automationCredits = {}, automationLoadError, isLoadingAutomations: isLoading, onArchiveAutomation, onDeleteAutomation, onEditAutomation, onRestoreAutomation, onRefreshAutomations, onRunAutomationNow, onToggleAutomation, } = useSuperagentAutomations(); const bi = useAgentBi(); const [viewTab, setViewTab] = useState<'active' | 'archived'>('active'); const switchView = (view: 'active' | 'archived') => { if (view === viewTab) return; // no-op re-tap of the active tab void bi.trackEditor('Automations View Switch', { view }); setViewTab(view); }; const sortedAutomations = useMemo( () => [...automations].sort((a, b) => getAutomationTime(b) - getAutomationTime(a)), [automations], ); const activeAutomations = sortedAutomations.filter((automation) => !automation.is_archived); const archivedAutomations = sortedAutomations.filter((automation) => automation.is_archived); if (isLoading && automations.length === 0) { return ( Loading automations... ); } const displayAutomations = viewTab === 'archived' ? archivedAutomations : activeAutomations; return ( Automations {displayAutomations.length} {automationLoadError ? ( Couldn’t load automations. void onRefreshAutomations?.(agent.id)} style={({ pressed }) => [ editorShellStyles.automationRetryButton, pressed && styles.pressed, ]} > Retry ) : null} switchView('active')} style={({ pressed }) => [ editorShellStyles.automationTabButton, viewTab === 'active' && editorShellStyles.automationTabButtonActive, pressed && styles.pressed, ]} > Active {activeAutomations.length} switchView('archived')} style={({ pressed }) => [ editorShellStyles.automationTabButton, viewTab === 'archived' && editorShellStyles.automationTabButtonActive, pressed && styles.pressed, ]} > Archived {archivedAutomations.length} {displayAutomations.length > 0 ? ( {displayAutomations.map((automation) => ( ))} ) : automationLoadError ? null : ( {viewTab === 'archived' ? ( ) : ( )} {viewTab === 'archived' ? 'No archived automations' : 'No automations yet'} {viewTab === 'archived' ? 'Automations you archive will appear here.' : 'Ask Superagent to create a scheduled or connector task.'} )} ); } type AutomationPendingAction = 'archive' | 'delete' | 'edit' | 'restore' | 'run' | 'toggle'; function AutomationCard({ agentId, automation, credits, onArchiveAutomation, onDeleteAutomation, onEditAutomation, onRestoreAutomation, onRunAutomationNow, onToggleAutomation, }: { agentId: string; automation: SuperagentAutomation; credits?: SuperagentAutomationCreditsSummary; onArchiveAutomation?: (input: SuperagentAutomationActionInput) => Promise | void; onDeleteAutomation?: (input: SuperagentAutomationActionInput) => Promise | void; onEditAutomation?: (input: SuperagentAutomationActionInput) => Promise | void; onRestoreAutomation?: (input: SuperagentAutomationActionInput) => Promise | void; onRunAutomationNow?: (input: SuperagentAutomationActionInput) => Promise | void; onToggleAutomation?: (input: SuperagentAutomationActionInput) => Promise | void; }) { const [expanded, setExpanded] = useState(false); const [pendingAction, setPendingAction] = useState(null); const Icon = AUTOMATION_ICONS[automation.automation_type] ?? Timer; const trigger = describeAutomationTrigger(automation); const lastRun = formatDateTime(automation.last_run_at); const isArchived = !!automation.is_archived; const isActive = !isArchived && automation.is_active !== false; const canRunNow = automation.automation_type === 'scheduled' && !isArchived && !!onRunAutomationNow; const actionInput = { agentId, automation }; const runAction = async ( action: AutomationPendingAction, handler?: (input: SuperagentAutomationActionInput) => Promise | void, ) => { if (!handler || pendingAction) { return; } setPendingAction(action); try { await handler(actionInput); } finally { setPendingAction(null); } }; return ( {automation.name} {trigger ? ( {trigger} ) : null} {automation.description ? ( {automation.description} ) : null} {formatAutomationType(automation.automation_type)} {automation.function_name ? ( {automation.function_name} ) : null} {credits ? `${formatCredits(credits.total)} credits used` : 'Consumes credits'} {formatRuns(automation)} {lastRun ? ` · Last run ${lastRun}` : ''} setExpanded((current) => !current)} /> {onEditAutomation ? ( runAction('edit', onEditAutomation)} /> ) : null} {canRunNow ? ( runAction('run', onRunAutomationNow)} /> ) : null} {!isArchived && onToggleAutomation ? ( runAction('toggle', onToggleAutomation)} /> ) : null} {!isArchived && onArchiveAutomation ? ( runAction('archive', onArchiveAutomation)} /> ) : null} {isArchived && onRestoreAutomation ? ( runAction('restore', onRestoreAutomation)} /> ) : null} {onDeleteAutomation ? ( runAction('delete', onDeleteAutomation)} /> ) : null} {expanded ? ( {trigger ? ( ) : null} {automation.description ? ( ) : null} {automation.function_name ? ( ) : null} {automation.function_args && Object.keys(automation.function_args).length > 0 ? ( ) : null} {automation.total_runs ?? 0} total {automation.successful_runs ?? 0} succeeded {automation.failed_runs ?? 0} failed {credits ? ( Total credits: {formatCredits(credits.total)} {credits.since ? ` · since ${formatDate(credits.since) ?? credits.since}` : ''} ) : null} {lastRun ? ( Last run: {lastRun} {automation.last_run_status ? ` · ${automation.last_run_status}` : ''} ) : null} {automation.created_date ? ( Created {formatDate(automation.created_date) ?? automation.created_date} ) : null} ) : null} ); } function AutomationActionButton({ danger, icon: Icon, isLoading, label, onPress, }: { danger?: boolean; icon: LucideIcon; isLoading?: boolean; label: string; onPress: () => void; }) { return ( [ editorShellStyles.automationActionButton, danger && editorShellStyles.automationActionButtonDanger, pressed && styles.pressed, isLoading && editorShellStyles.automationActionButtonDisabled, ]} > {isLoading ? ( ) : ( )} {label} ); } function AutomationDetailBlock({ label, monospace, value, }: { label: string; monospace?: boolean; value: string; }) { return ( {label} {value} ); } function AutomationStatusBadge({ automation }: { automation: SuperagentAutomation }) { const isArchived = !!automation.is_archived; const isActive = !isArchived && automation.is_active !== false; const label = isArchived ? 'Archived' : isActive ? 'Active' : 'Paused'; return ( {label} ); } const WORKFLOW_TRIGGER_ICONS: Record = { scheduled: Timer, connector: Webhook, entity: Zap, in_app_agent: GitBranch, app_user_auth: GitBranch, }; function WorkflowsPanel({ agent }: { agent: SuperagentAgent }) { const { workflows = [], workflowLoadError, isLoadingWorkflows: isLoading, onArchiveWorkflow, onRestoreWorkflow, onRefreshWorkflows, onRunWorkflowNow, onToggleWorkflow, } = useSuperagentWorkflows(); const bi = useAgentBi(); const [viewTab, setViewTab] = useState<'active' | 'archived'>('active'); const switchView = (view: 'active' | 'archived') => { if (view === viewTab) return; // no-op re-tap of the active tab void bi.trackEditor('Workflows View Switch', { view }); setViewTab(view); }; // Sort once and partition in the same pass; both partitions feed the tab // badges, so neither can be skipped. Recompute only when the list changes, // not on every Active/Archived tab toggle. const { activeWorkflows, archivedWorkflows } = useMemo(() => { const sorted = sortWorkflows(workflows); return { activeWorkflows: sorted.filter((workflow) => !isWorkflowArchived(workflow)), archivedWorkflows: sorted.filter((workflow) => isWorkflowArchived(workflow)), }; }, [workflows]); if (isLoading && workflows.length === 0) { return ( Loading tasks... ); } const displayWorkflows = viewTab === 'archived' ? archivedWorkflows : activeWorkflows; return ( Tasks {displayWorkflows.length} {workflowLoadError ? ( Couldn’t load tasks. void onRefreshWorkflows?.(agent.id)} style={({ pressed }) => [ editorShellStyles.automationRetryButton, pressed && styles.pressed, ]} > Retry ) : null} switchView('active')} style={({ pressed }) => [ editorShellStyles.automationTabButton, viewTab === 'active' && editorShellStyles.automationTabButtonActive, pressed && styles.pressed, ]} > Active {activeWorkflows.length} switchView('archived')} style={({ pressed }) => [ editorShellStyles.automationTabButton, viewTab === 'archived' && editorShellStyles.automationTabButtonActive, pressed && styles.pressed, ]} > Archived {archivedWorkflows.length} {displayWorkflows.length > 0 ? ( {displayWorkflows.map((workflow) => ( ))} ) : workflowLoadError ? null : ( {viewTab === 'archived' ? ( ) : ( )} {viewTab === 'archived' ? 'No archived tasks' : 'No tasks yet'} {viewTab === 'archived' ? 'Tasks you archive will appear here.' : 'Ask Superagent to create a scheduled or triggered task.'} )} ); } type WorkflowPendingAction = 'archive' | 'restore' | 'run' | 'toggle'; function WorkflowCard({ agentId, onArchiveWorkflow, onRestoreWorkflow, onRunWorkflowNow, onToggleWorkflow, workflow, }: { agentId: string; onArchiveWorkflow?: (input: SuperagentWorkflowActionInput) => Promise | void; onRestoreWorkflow?: (input: SuperagentWorkflowActionInput) => Promise | void; onRunWorkflowNow?: (input: SuperagentWorkflowActionInput) => Promise | void; onToggleWorkflow?: (input: SuperagentWorkflowActionInput) => Promise | void; workflow: SuperagentWorkflow; }) { const [expanded, setExpanded] = useState(false); const [pendingAction, setPendingAction] = useState(null); const Icon = WORKFLOW_TRIGGER_ICONS[workflow.trigger?.config?.trigger_type ?? ''] ?? Timer; const trigger = describeWorkflowTrigger(workflow); const lastRun = formatDateTime(workflow.last_run_at); const archived = isWorkflowArchived(workflow); const active = isWorkflowActive(workflow); const deactivatedByFailures = isDeactivatedByFailures(workflow); const actionInput = { agentId, workflow }; const runAction = async ( action: WorkflowPendingAction, handler?: (input: SuperagentWorkflowActionInput) => Promise | void, ) => { if (!handler || pendingAction) { return; } setPendingAction(action); try { await handler(actionInput); } finally { setPendingAction(null); } }; return ( {workflow.name} {trigger ? ( {trigger} ) : null} {workflow.description ? ( {workflow.description} ) : null} {deactivatedByFailures ? ( Paused after repeated failures ) : null} {formatWorkflowRuns(workflow)} {lastRun ? ` · Last run ${lastRun}` : ''} {workflow.last_run_status ? ` · ${workflow.last_run_status}` : ''} setExpanded((current) => !current)} /> {!archived && onRunWorkflowNow ? ( runAction('run', onRunWorkflowNow)} /> ) : null} {!archived && onToggleWorkflow ? ( runAction('toggle', onToggleWorkflow)} /> ) : null} {!archived && onArchiveWorkflow ? ( runAction('archive', onArchiveWorkflow)} /> ) : null} {archived && onRestoreWorkflow ? ( runAction('restore', onRestoreWorkflow)} /> ) : null} {expanded ? ( {trigger ? ( ) : null} {workflow.description ? ( ) : null} {formatWorkflowRuns(workflow)} {lastRun ? ( Last run: {lastRun} {workflow.last_run_status ? ` · ${workflow.last_run_status}` : ''} ) : null} {workflow.created_date ? ( Created {formatDate(workflow.created_date) ?? workflow.created_date} ) : null} ) : null} ); } function WorkflowStatusBadge({ workflow }: { workflow: SuperagentWorkflow }) { const active = isWorkflowActive(workflow); const label = formatWorkflowStatus(workflow); return ( {label} ); } const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function describeAutomationTrigger(automation: SuperagentAutomation) { if (automation.automation_type === 'scheduled') { return describeSchedule(automation); } if (automation.automation_type === 'entity') { const events = automation.event_types?.length ? automation.event_types.join(', ') : 'changes'; return automation.entity_name ? `${automation.entity_name} · ${events}` : events; } if (automation.automation_type === 'connector') { const connectorName = getConnectorDisplayName(automation.integration_type); const events = automation.events?.length ? automation.events.join(', ') : ''; return events ? `${connectorName} · ${events}` : connectorName; } return null; } function describeSchedule(automation: SuperagentAutomation) { if (automation.schedule_mode === 'one-time' && automation.one_time_date) { return `One-time · ${formatDateTime(automation.one_time_date) ?? automation.one_time_date}`; } if (automation.schedule_type === 'cron' && automation.cron_expression) { return `Cron · ${automation.cron_expression}`; } const parts: string[] = []; if (automation.repeat_interval && automation.repeat_unit) { const unit = automation.repeat_interval === 1 ? automation.repeat_unit.replace(/s$/, '') : automation.repeat_unit; parts.push(`Every ${automation.repeat_interval === 1 ? '' : `${automation.repeat_interval} `}${unit}`.trim()); } if (automation.repeat_unit === 'weeks' && automation.repeat_on_days?.length) { parts.push(`on ${automation.repeat_on_days.map((day) => DAY_NAMES[day]).filter(Boolean).join(', ')}`); } if (automation.repeat_unit === 'months' && automation.repeat_on_day_of_month) { parts.push(`on day ${automation.repeat_on_day_of_month}`); } if (automation.start_time) { parts.push(`at ${automation.start_time}`); } if (automation.ends_type === 'on' && automation.ends_on_date) { parts.push(`until ${formatDate(automation.ends_on_date) ?? automation.ends_on_date}`); } else if (automation.ends_type === 'after' && automation.ends_after_count) { parts.push(`for ${automation.ends_after_count} runs`); } return parts.length > 0 ? parts.join(' ') : null; } function formatAutomationType(type: SuperagentAutomation['automation_type']) { if (type === 'scheduled') { return 'Scheduled'; } if (type === 'entity') { return 'Entity trigger'; } if (type === 'connector') { return 'Connector trigger'; } return String(type); } function formatRuns(automation: SuperagentAutomation) { const total = automation.total_runs ?? 0; const successful = automation.successful_runs ?? 0; const failed = automation.failed_runs ?? 0; return `${total} runs · ${successful} succeeded · ${failed} failed`; } function formatCredits(value: number) { return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, ''); } function getAutomationTime(automation: SuperagentAutomation) { const dateValue = automation.updated_date ?? automation.created_date ?? automation.last_run_at; if (!dateValue) { return 0; } const timestamp = Date.parse(dateValue); return Number.isNaN(timestamp) ? 0 : timestamp; } function getConnectorDisplayName(connectorId?: string) { if (!connectorId) { return 'Connector'; } return SUPERAGENT_CONNECTOR_CATALOG.find((connector) => connector.id === connectorId)?.name ?? connectorId .split(/[_-]/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } function formatDate(value?: string | null) { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleDateString(); } function formatDateTime(value?: string | null) { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleString(); } function PlaceholderSettingsPanel({ tab }: { tab: SuperagentEditorTab }) { const label = getEditorTabLabel(tab); const iconMap: Partial> = { files: FileText, automations: Settings, }; const Icon = iconMap[tab] ?? Settings; return ( {label} This section now lives in the settings modal. The next slice can replace this placeholder with the matching native controls. ); }