import React, { useCallback, useImperativeHandle, useMemo, useState, type Ref } from 'react'; import { KeyboardAvoidingView, Platform } from 'react-native'; import { SafeAreaView, type Edge } from 'react-native-safe-area-context'; import { AgentFeatureMenu } from '../settings/AgentFeatureMenu'; import { ConversationChat } from './ConversationChat'; import { ConversationFeatureNavigationProvider } from '../../featureNavigationContext'; import { ConversationHeader } from './conversationParts'; import { conversationStyles } from './conversationStyles'; import { EditorDrawer } from '../editor/EditorDrawer'; import { featureRefreshKeys } from '../settings/featureMenu'; import { useAgentBi } from '../../analytics/mixpanelContext'; import { getSelectableModelOptions, resolveModelOption } from '../settings/modelOptions'; import { ModelPickerModal, useModelPicker } from '../settings/modelPicker'; import { RenameAgentModal } from '../settings/RenameAgentModal'; import { takePendingFirstMessage } from '../../runtime/pendingFirstMessage'; import { useFeatureFlags, useSuperagentModelAccess } from '../../user/userContext'; import { useSuperagentAgents, useSuperagentAutomations, useSuperagentChannels, useSuperagentCollaborators, useSuperagentConversationRuntime, useSuperagentFiles, useSuperagentModelActions, useSuperagentSecrets, useSuperagentShellOptions, useSuperagentWorkflows, } from '../../runtime/runtimeContext'; import type { SuperagentAgent, SuperagentEditorTab } from '../../types'; import { useSuperagentConversation } from './useSuperagentConversation'; // When the host hides the in-package header (`hideHeader`) it owns the conversation // chrome itself: on iOS that's a draw-behind translucent glass nav bar, and the host // folds the full top inset (status bar + nav bar) into `contentTopInset`. Dropping the // top safe-area edge here lets the scroll content flow up under that glass bar instead // of being capped by an opaque status-bar strip that makes the bar read as flat. const HIDDEN_HEADER_SAFE_AREA_EDGES: Edge[] = ['left', 'right', 'bottom']; /** * Imperative actions the host can trigger when the in-package header is hidden * (`hideHeader`). Surfaced to the host through the SuperagentHomeScreen ref. */ export type ConversationControls = { openMenu: () => void; openShare: () => void; openRename: () => void; }; export function ConversationScreen({ agent, controlsRef, onBack, }: { agent: SuperagentAgent; controlsRef?: Ref; onBack: () => void; }) { // The prompt typed on the home composer, for a just-created agent: claimed once // per mount so it is auto-sent exactly once and can never come back afterwards. const [firstMessage] = useState(() => takePendingFirstMessage(agent.id)); // Runtime state/handlers come from context, not props — this screen owns only // its local UI state (open menu/modals, model picker) and forwards genuine // identity/UI props (agent, isVisible, onClose) to its children. const { apiClient, realtimeClient, currentUserId, latestMessages, messagesByAgentId, onAgentMessageDone } = useSuperagentConversationRuntime(); const { hideConversationHeader, onViewPlans } = useSuperagentShellOptions(); const { onUpdateAgentModel } = useSuperagentModelActions(); const { onRenameAgent } = useSuperagentAgents(); const { onRefreshAutomations } = useSuperagentAutomations(); const { onRefreshWorkflows } = useSuperagentWorkflows(); const { onRefreshFiles } = useSuperagentFiles(); const { onRefreshChannels } = useSuperagentChannels(); const { onRefreshAgentSettings } = useSuperagentSecrets(); const { onRefreshCollaborators } = useSuperagentCollaborators(); const bi = useAgentBi(); const hideHeader = hideConversationHeader ?? false; const fallbackMessages = messagesByAgentId[agent.id] ?? latestMessages; // The header "more" menu lists each secondary feature; tapping one opens that // feature's own modal. `activeFeature` is the editor tab the modal shows (null // when closed); the menu's own visibility is `isMenuOpen`. const [isMenuOpen, setIsMenuOpen] = useState(false); const [activeFeature, setActiveFeature] = useState(null); const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [renameError, setRenameError] = useState(null); // Open a feature modal and refresh its data on open (the per-feature refresh // the tabbed drawer used to fire on tab switch). const openFeature = useCallback((tab: SuperagentEditorTab) => { setIsMenuOpen(false); void bi.trackEditor('Feature Opened', { feature: tab }); // Opening the sharing drawer is the native "invite" entry point; is_owner isn't // reliably known here (no role on the agent), so it's omitted. if (tab === 'sharing') void bi.trackEditor('Collaboration Invite Click'); for (const key of featureRefreshKeys(tab)) { // The Tasks tab shows workflows OR automations depending on the agent; // refresh the matching domain so a workflow just created in this chat is // reflected on open (native has no realtime workflow invalidation). if (key === 'automations') { if (agent.workflowsEnabled) onRefreshWorkflows?.(agent.id); else onRefreshAutomations?.(agent.id); } else if (key === 'files') onRefreshFiles?.(agent.id); else if (key === 'channels') onRefreshChannels?.(agent.id); else if (key === 'agentSettings') onRefreshAgentSettings?.(agent.id); else if (key === 'collaborators') onRefreshCollaborators?.(agent.id); } setActiveFeature(tab); }, [agent.id, agent.workflowsEnabled, bi, onRefreshAgentSettings, onRefreshAutomations, onRefreshWorkflows, onRefreshChannels, onRefreshCollaborators, onRefreshFiles]); // The composer's model pill opens a dedicated chat-model picker sheet (not the // whole General modal), reusing the shared picker hook. const { getFlagVariant, hasFlag } = useFeatureFlags(); const { canSelectBestModel } = useSuperagentModelAccess(); const modelOptions = useMemo( () => getSelectableModelOptions(hasFlag, getFlagVariant), [hasFlag, getFlagVariant], ); const chatSelected = useMemo(() => resolveModelOption(agent.model), [agent.model]); const chatModelPicker = useModelPicker({ agentId: agent.id, selected: chatSelected, options: modelOptions, onSelect: onUpdateAgentModel, canSelectBestModel, onViewPlans, }); // Expose the header actions imperatively so a host-supplied (e.g. native) nav bar can // drive them when `hideHeader` is set. share/rename only act when the host wired the // corresponding handler, mirroring the in-package header's button-visibility gating. useImperativeHandle(controlsRef, () => ({ openMenu: () => setIsMenuOpen(true), openShare: () => openFeature('sharing'), openRename: () => setIsRenameModalOpen(true), }), [openFeature]); const refreshAutomations = useCallback(() => { return onRefreshAutomations?.(agent.id); }, [agent.id, onRefreshAutomations]); const conversation = useSuperagentConversation({ agentId: agent.id, apiClient, currentUserId, fallbackMessages, fallbackSending: false, onAgentMessageDone, onConversationSettled: refreshAutomations, realtimeClient, // A seeded prompt becomes the first turn, so skip the welcome intro before it. skipIntro: firstMessage != null, }); const saveAgentName = async (name: string) => { if (!onRenameAgent) { return; } setIsRenaming(true); setRenameError(null); try { await onRenameAgent({ agentId: agent.id, name }); setIsRenameModalOpen(false); } catch (error) { setRenameError(error instanceof Error ? error.message : 'Failed to rename Superagent'); } finally { setIsRenaming(false); } }; return ( {hideHeader ? null : ( setIsMenuOpen(true)} onOpenShare={() => openFeature('sharing')} onRenameRequest={() => setIsRenameModalOpen(true)} /> )} setIsMenuOpen(false)} onSelectFeature={openFeature} visible={isMenuOpen} /> setActiveFeature(null)} /> { if (!isRenaming) { setIsRenameModalOpen(false); setRenameError(null); } }} onSave={saveAgentName} /> ); }