import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type ForwardedRef } from 'react'; import { themedColor } from '../../theme'; import { ActivityIndicator, FlatList, RefreshControl, SafeAreaView, Text, View, } from 'react-native'; import { Pencil, Trash2 } from 'lucide-react-native'; import { SuperagentScreen } from './SuperagentScreen'; import { SuperagentTopBar } from './SuperagentTopBar'; import { NoAgentsHero } from './NoAgentsHero'; import { PlaceholderPage } from './screenParts'; import { ChatConversationRow, CreateAgentRow } from './chatListParts'; import { pickCreateAgentSubtitle } from './createAgentSubtitle'; import { AgentActionsSheet, ConfirmDialog, type AgentAction } from '../settings/agentActionsParts'; import { RenameAgentModal } from '../settings/RenameAgentModal'; import { ConversationScreen, type ConversationControls } from '../conversation/ConversationScreen'; import { setPendingFirstMessage } from '../../runtime/pendingFirstMessage'; import { routeIdentityKey } from '../../runtime/routeKey'; import { useAgentBi } from '../../analytics/mixpanelContext'; import { useSuperagentAgents, useSuperagentConversationRuntime, useSuperagentNavigation, useSuperagentShellOptions, } from '../../runtime/runtimeContext'; import { styles } from '../../styles'; import type { SuperagentAgent, SuperagentCreateAgentInput, SuperagentHomeScreenHandle, SuperagentRoute } from '../../types'; const DEFAULT_ROUTE: SuperagentRoute = { name: 'home' }; /** * The Superagent home/conversation view. Pure UI over the full runtime prop * surface — the exported `SuperagentHomeScreen` shell mounts the theme + user * providers and feeds this from `useSuperagentRuntime`. Not part of the public * package API (used directly only by the in-repo web demo harness). */ export function SuperagentHomeScreenView({ handleRef, }: { handleRef?: ForwardedRef; }) { const { agents, isLoading, isRefreshingAgents, onRefreshAgents, onCreateAgent, onRenameAgent, onDeleteAgent, } = useSuperagentAgents(); const { navigationMode = 'internal', initialRoute = DEFAULT_ROUTE, onOpenAgent, onAgentBack, onRouteChange, } = useSuperagentNavigation(); const { latestMessages, messagesByAgentId } = useSuperagentConversationRuntime(); const { isActive } = useSuperagentShellOptions(); // The mobile home screen shows a single flat agent list under a fixed title // (folders aren't part of the mobile runtime surface). const homeTitle = 'Superagents'; const [route, setRoute] = useState(initialRoute); const [isCreating, setIsCreating] = useState(false); // Bridges the host-facing ref to the active conversation's imperative controls. // Null while no conversation is mounted, so each handle method becomes a safe no-op. const conversationControlsRef = useRef(null); useImperativeHandle(handleRef, () => ({ openConversationMenu: () => conversationControlsRef.current?.openMenu(), openConversationShare: () => conversationControlsRef.current?.openShare(), openConversationRename: () => conversationControlsRef.current?.openRename(), }), []); // Track app_id (the active agent's id) as a super-property so every event // carries it; null on the home list. The agent-editor page-view fires in the // same effect, after register(), so it never lands on a stale app_id. Keyed on // the agent id so it fires once per agent open (home → agent, agent → agent), // not on re-renders of the same route. const bi = useAgentBi(); const lastViewedAgentId = useRef(null); useEffect(() => { const agentId = route.name === 'agent' ? route.agentId : null; bi.register({ app_id: agentId }); if (agentId && agentId !== lastViewedAgentId.current) { void bi.trackEditor('Page View'); } lastViewedAgentId.current = agentId; }, [bi, route]); // Home page-view, fired each time the home list becomes visible. The tab stays // mounted, so mount alone doesn't mean the user saw it — gate on isActive. Also // gate on the list having loaded so agent_count isn't logged as 0 for returning // users while agents (default []) is still loading. const wasViewingHome = useRef(false); const isViewingHome = isActive === true && route.name === 'home'; useEffect(() => { if (isViewingHome && !isLoading && !wasViewingHome.current) { wasViewingHome.current = true; void bi.track('Agent Home: Page View', { navigation_mode: navigationMode, agent_count: agents.length }); } else if (!isViewingHome) { wasViewingHome.current = false; } }, [isViewingHome, isLoading, agents.length, bi, navigationMode]); // Follow externally-driven route changes (e.g. the runtime moves home after the // active agent is deleted, or a deep link changes the agent). Keyed by value so // it doesn't clobber internal navigation on unrelated re-renders. const externalRouteKey = routeIdentityKey(initialRoute); useEffect(() => { setRoute(initialRoute); // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalRouteKey]); const latestAgent = agents[0] ?? null; // Resolve the conversation feed for an agent: the explicit latestMessages feed // covers the most-recent agent, with the per-agent map as the general source — // so each chat row shows a preview regardless of how the host wires data. const messagesForAgent = useCallback((agent: SuperagentAgent) => { const mapped = messagesByAgentId[agent.id]; if (mapped?.length) return mapped; if (agent.id === latestAgent?.id && latestMessages.length) return latestMessages; return mapped ?? []; }, [latestAgent?.id, latestMessages, messagesByAgentId]); const navigate = useCallback((nextRoute: SuperagentRoute) => { setRoute(nextRoute); onRouteChange?.(nextRoute); }, [onRouteChange]); const openAgent = useCallback((agentId: string) => { if (navigationMode === 'internal') { navigate({ name: 'agent', agentId }); } onOpenAgent?.(agentId); }, [navigate, navigationMode, onOpenAgent]); const createAgent = useCallback(async (input?: SuperagentCreateAgentInput) => { void bi.trackHome('Create Agent Click', { is_new_user: agents.length === 0, idea_key: input?.ideaKey, idea_category: input?.ideaCategory, }); if (!onCreateAgent) { navigate({ name: 'create-agent' }); return; } setIsCreating(true); try { const createdAgent = await onCreateAgent(input); if (createdAgent?.id) { void bi.trackHome('Agent Created', { agent_id: createdAgent.id }); const content = input?.initialMessage?.trim(); // Hand the composer prompt + connectors to the new agent's chat, which // auto-sends it. Kept out of the route so it can't be replayed (see // pendingFirstMessage). if (content) setPendingFirstMessage({ agentId: createdAgent.id, content, connectorIds: input?.connectorIds }); openAgent(createdAgent.id); } else { navigate({ name: 'create-agent' }); } } finally { setIsCreating(false); } }, [agents.length, bi, navigate, onCreateAgent, openAgent]); // Per-agent overflow menu (mirrors the web "all agents" card actions). const [menuAgent, setMenuAgent] = useState(null); const [renameTarget, setRenameTarget] = useState(null); const [isRenaming, setIsRenaming] = useState(false); const [renameError, setRenameError] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const hasAnyAgentAction = Boolean(onRenameAgent || onDeleteAgent); const menuActions = useMemo(() => { if (!menuAgent) return []; const agent = menuAgent; const close = () => setMenuAgent(null); const iconColor = themedColor('#F7F7F7'); const actions: AgentAction[] = []; if (onRenameAgent) { actions.push({ key: 'rename', label: 'Rename', icon: , onPress: () => { close(); setRenameError(null); setRenameTarget(agent); }, }); } if (onDeleteAgent) { actions.push({ key: 'delete', label: 'Delete', destructive: true, icon: , onPress: () => { void bi.trackEditor('Agent Delete Click'); close(); setDeleteTarget(agent); }, }); } return actions; }, [bi, menuAgent, onDeleteAgent, onRenameAgent]); const handleRename = useCallback(async (name: string) => { if (!renameTarget || !onRenameAgent) return; setIsRenaming(true); setRenameError(null); try { await onRenameAgent({ agentId: renameTarget.id, name }); setRenameTarget(null); } catch (error) { setRenameError(error instanceof Error ? error.message : 'Failed to rename Superagent'); } finally { setIsRenaming(false); } }, [onRenameAgent, renameTarget]); const handleDelete = useCallback(async () => { if (!deleteTarget || !onDeleteAgent) return; setIsDeleting(true); try { await onDeleteAgent({ agentId: deleteTarget.id }); setDeleteTarget(null); } finally { setIsDeleting(false); } }, [deleteTarget, onDeleteAgent]); // Picked once per mount so the copy varies across app opens but stays // stable while the screen re-renders. const [createAgentSubtitle] = useState(() => pickCreateAgentSubtitle(new Date())); const renderConversationRow = useCallback((agent: SuperagentAgent) => ( { // agent_id passed explicitly — the app_id super-property is null on the home list. void bi.trackHome('Continue Agent Click', { agent_id: agent.id }); openAgent(agent.id); }} onLongPress={hasAnyAgentAction ? () => setMenuAgent(agent) : undefined} /> ), [bi, hasAnyAgentAction, messagesForAgent, openAgent]); if (route.name === 'agent') { const activeAgent = findAgentById(agents, route.agentId); return ( { if (onAgentBack) { onAgentBack(); return; } navigate({ name: 'home' }); }} /> ); } if (route.name === 'create-agent') { return ( navigate({ name: 'home' })} /> ); } const hasAgents = agents.length > 0; return ( {/* The shell is edge-to-edge; this screen owns its safe-area insets so the top bar / title / chips don't render under the notch. */} {/* Native DS top bar: centered logo (no hamburger / workspace button — those live in the sidebar, Phase 2). */} {isLoading ? ( ) : !hasAgents ? ( ) : ( agent.id} refreshControl={ onRefreshAgents ? ( { void onRefreshAgents(); }} tintColor={themedColor('#FF5A1F')} colors={[themedColor('#FF5A1F')]} /> ) : undefined } renderItem={({ item }) => renderConversationRow(item)} // Title lives in the list header (not pinned above) so the pull-to-refresh // spinner surfaces at the very top of the screen, above the title. ListHeaderComponent={homeTitle ? {homeTitle} : null} ListEmptyComponent={ No agents in this folder yet. } ListFooterComponent={ createAgent()} /> } /> )} setMenuAgent(null)} /> { if (!isRenaming) { setRenameTarget(null); setRenameError(null); } }} onSave={handleRename} /> { if (!isDeleting) setDeleteTarget(null); }} onConfirm={handleDelete} /> ); } function findAgentById(agents: SuperagentAgent[], agentId: string) { return agents.find((agent) => agent.id === agentId); } function createUnknownAgent(agentId: string): SuperagentAgent { return { id: agentId, name: 'Superagent', }; }