import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Pressable, type PressableStateCallbackType, Text, View } from "react-native"; import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import type { DaemonClient, FetchRecentProviderSessionEntry, } from "@getpaseo/client/internal/daemon-client"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import { ChevronDown, Inbox, Layers, RotateCw } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; import { getProviderIcon } from "@/components/provider-icons"; import { formatTimeAgo } from "@/utils/time"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { i18n } from "@/i18n/i18next"; import { aggregateSessionEntries, ALL_FILTER_VALUE, buildProviderLabelMap, collectErroredProviderLabels, computeEmptyState, getPromptPreview, getSessionTitle, PER_PROVIDER_LIMIT, resolveProvidersToFetch, sumFilteredAlreadyImportedCount, } from "@/components/import-session-sheet-view-model"; const IMPORT_SHEET_SNAP_POINTS = ["70%", "92%"]; const DISABLED_ACCESSIBILITY_STATE = { disabled: true }; type RecentProviderSessionsClient = Pick< DaemonClient, "fetchRecentProviderSessions" | "importAgent" >; type ImportedAgent = Awaited>; interface ImportSessionSheetProps { visible: boolean; client: RecentProviderSessionsClient | null; serverId: string | null; cwd?: string | null; onClose: () => void; onImportedAgent?: (agentId: string) => void; onImported?: (agent: ImportedAgent) => void; } type RecentSessionsResponse = Awaited< ReturnType >; interface SessionsQueryConfig { queryKey: ReadonlyArray; enabled: boolean; queryFn: () => Promise; } function buildSessionsQueriesConfig(args: { providersToFetch: AgentProvider[] | null; sessionsQueryRoot: ReadonlyArray; visible: boolean; client: RecentProviderSessionsClient | null; cwd: string | null | undefined; hostDisconnectedMessage?: string; }): SessionsQueryConfig[] { const { providersToFetch, sessionsQueryRoot, visible, client, cwd, hostDisconnectedMessage } = args; if (providersToFetch === null) return []; const enabled = visible && Boolean(client); return providersToFetch.map((provider) => ({ queryKey: [...sessionsQueryRoot, provider], enabled, queryFn: async () => { if (!client) { throw new Error(hostDisconnectedMessage ?? i18n.t("workspace.terminal.hostDisconnected")); } return await client.fetchRecentProviderSessions({ ...(cwd ? { cwd } : {}), providers: [provider], limit: PER_PROVIDER_LIMIT, }); }, })); } interface SheetStatusMessagesProps { isClientReady: boolean; isSnapshotUnsupported: boolean; hasNoImportableProviders: boolean; isLoadingSessions: boolean; hasRows: boolean; allQueriesErrored: boolean; erroredProviderLabels: ReadonlyArray; importErrored: boolean; } function SheetStatusMessages({ isClientReady, isSnapshotUnsupported, hasNoImportableProviders, isLoadingSessions, hasRows, allQueriesErrored, erroredProviderLabels, importErrored, }: SheetStatusMessagesProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); if (!isClientReady) { return {t("importSession.status.connectHost")}; } if (isSnapshotUnsupported) { return {t("importSession.status.updateHost")}; } return ( <> {hasNoImportableProviders ? ( {t("importSession.status.noProviders")} ) : null} {isLoadingSessions && !hasRows ? ( {t("importSession.status.loading")} ) : null} {allQueriesErrored ? ( {t("importSession.status.failedAll")} ) : null} {!allQueriesErrored && erroredProviderLabels.length > 0 ? ( {t("importSession.status.failedProviders", { providers: erroredProviderLabels.join(", "), })} ) : null} {importErrored ? ( {t("importSession.status.failedImport")} ) : null} ); } function RefreshAction({ isRefreshing, onPress }: { isRefreshing: boolean; onPress: () => void }) { const { theme } = useUnistyles(); const { t } = useTranslation(); const pressableStyle = useCallback( ({ pressed }: PressableStateCallbackType) => [ styles.refreshButton, pressed && styles.refreshButtonPressed, ], [], ); return ( {isRefreshing ? ( ) : ( )} ); } function SheetEmptyState({ title }: { title: string }) { const { theme } = useUnistyles(); return ( {title} ); } function ImportSessionSheetRow({ entry, disabled, importing, showCwd, onImportSession, }: { entry: FetchRecentProviderSessionEntry; disabled: boolean; importing: boolean; showCwd: boolean; onImportSession: (entry: FetchRecentProviderSessionEntry) => void; }) { const { theme } = useUnistyles(); const { t } = useTranslation(); const title = getSessionTitle(entry); const promptPreview = getPromptPreview(entry); const lastActivity = formatTimeAgo(new Date(entry.lastActivityAt)); const ProviderIcon = getProviderIcon(entry.providerId); const accessibilityState = useMemo( () => (disabled ? DISABLED_ACCESSIBILITY_STATE : undefined), [disabled], ); const handlePress = useCallback(() => { onImportSession(entry); }, [entry, onImportSession]); const pressableStyle = useCallback( ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.row, Boolean(hovered) && styles.rowHovered, pressed && styles.rowPressed, ], [], ); return ( {title} {importing ? t("importSession.row.importing") : lastActivity} {promptPreview} {showCwd && entry.cwd ? ( {entry.cwd} ) : null} ); } export function ImportSessionSheet({ visible, client, serverId, cwd, onClose, onImportedAgent, onImported, }: ImportSessionSheetProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { theme } = useUnistyles(); const { entries: snapshotEntries, supportsSnapshot } = useProvidersSnapshot(serverId, { cwd, enabled: visible, }); const providersToFetch = useMemo( () => resolveProvidersToFetch(supportsSnapshot, snapshotEntries), [supportsSnapshot, snapshotEntries], ); const providerLabelById = useMemo( () => buildProviderLabelMap(snapshotEntries), [snapshotEntries], ); const sessionsQueryRoot = useMemo( () => ["recent-provider-sessions", cwd ?? null] as const, [cwd], ); const queriesConfig = useMemo( () => buildSessionsQueriesConfig({ providersToFetch, sessionsQueryRoot, visible, client, cwd, hostDisconnectedMessage: t("workspace.terminal.hostDisconnected"), }), [providersToFetch, sessionsQueryRoot, visible, client, cwd, t], ); const queries = useQueries({ queries: queriesConfig }); const aggregatedEntries = useMemo(() => aggregateSessionEntries(queries), [queries]); const totalAlreadyImportedCount = useMemo( () => sumFilteredAlreadyImportedCount(queries), [queries], ); const filterProviders = useMemo(() => [...(providersToFetch ?? [])].sort(), [providersToFetch]); const [selectedProvider, setSelectedProvider] = useState(ALL_FILTER_VALUE); const [isFilterOpen, setIsFilterOpen] = useState(false); const filterAnchorRef = useRef(null); useEffect(() => { if ( !visible || (selectedProvider !== ALL_FILTER_VALUE && !filterProviders.includes(selectedProvider)) ) { setSelectedProvider(ALL_FILTER_VALUE); } }, [visible, filterProviders, selectedProvider]); const visibleEntries = useMemo(() => { if (selectedProvider === ALL_FILTER_VALUE) return aggregatedEntries; return aggregatedEntries.filter((entry) => entry.providerId === selectedProvider); }, [aggregatedEntries, selectedProvider]); const filterComboboxOptions = useMemo( () => [ { id: ALL_FILTER_VALUE, label: t("importSession.filters.all") }, ...filterProviders.map((provider) => ({ id: provider, label: providerLabelById.get(provider) ?? provider, })), ], [filterProviders, providerLabelById, t], ); const selectedProviderLabel = useMemo( () => filterComboboxOptions.find((opt) => opt.id === selectedProvider)?.label ?? t("importSession.filters.all"), [filterComboboxOptions, selectedProvider, t], ); const handleFilterOpen = useCallback(() => setIsFilterOpen(true), []); const filterTriggerStyle = useCallback( ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.filterTrigger, Boolean(hovered) && styles.filterTriggerHovered, pressed && styles.filterTriggerPressed, ], [], ); const handleFilterSelect = useCallback((id: string) => { setSelectedProvider(id); setIsFilterOpen(false); }, []); const filterOptionIcons = useMemo(() => { const map = new Map(); map.set(ALL_FILTER_VALUE, ); for (const provider of filterProviders) { const ProviderIcon = getProviderIcon(provider); map.set(provider, ); } return map; }, [filterProviders, theme.colors.foregroundMuted]); const renderFilterOption = useCallback( ({ option, selected, active, onPress, }: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void; }) => ( ), [filterOptionIcons], ); const importMutation = useMutation({ mutationFn: async (entry: FetchRecentProviderSessionEntry) => { if (!client) { throw new Error(t("workspace.terminal.hostDisconnected")); } if (!entry.cwd) { throw new Error("Session is missing a working directory"); } const agent = await client.importAgent({ providerId: entry.providerId, providerHandleId: entry.providerHandleId, cwd: entry.cwd, }); return agent; }, onSuccess: async (agent) => { await queryClient.invalidateQueries({ queryKey: sessionsQueryRoot }); onClose(); onImportedAgent?.(agent.id); onImported?.(agent); }, }); const importingSessionKey = importMutation.isPending && importMutation.variables ? `${importMutation.variables.providerId}:${importMutation.variables.providerHandleId}` : null; const handleImportSession = useCallback( (entry: FetchRecentProviderSessionEntry) => { importMutation.mutate(entry); }, [importMutation], ); const erroredProviderLabels = useMemo( () => collectErroredProviderLabels(providersToFetch, queries, providerLabelById), [queries, providersToFetch, providerLabelById], ); const isRefreshing = queries.some((query) => query.isFetching); const handleRefresh = useCallback(() => { void queryClient.invalidateQueries({ queryKey: sessionsQueryRoot }); }, [queryClient, sessionsQueryRoot]); const header = useMemo( () => ({ title: t("importSession.title"), actions: , }), [isRefreshing, handleRefresh, t], ); const isSnapshotUnsupported = !supportsSnapshot; const isWaitingForSnapshot = supportsSnapshot && snapshotEntries === undefined; const hasNoImportableProviders = providersToFetch !== null && providersToFetch.length === 0; const isQueryingProviders = queries.length > 0; const isLoadingSessions = isWaitingForSnapshot || (isQueryingProviders && queries.some((query) => query.isLoading || query.isPending)); const allQueriesErrored = isQueryingProviders && queries.every((query) => query.isError); const allQueriesSettled = isQueryingProviders && queries.every((query) => !query.isLoading && !query.isPending); const { showEmptyState, emptyStateTitle } = computeEmptyState({ isLoadingSessions, allQueriesErrored, isQueryingProviders, allQueriesSettled, selectedProvider, aggregatedCount: aggregatedEntries.length, visibleCount: visibleEntries.length, totalAlreadyImportedCount, providerLabelById, }); const showFilter = filterProviders.length > 1; return ( {showFilter ? ( {selectedProvider === ALL_FILTER_VALUE ? ( ) : ( (() => { const ProviderIcon = getProviderIcon(selectedProvider); return ; })() )} {selectedProviderLabel} ) : null} 0} allQueriesErrored={allQueriesErrored} erroredProviderLabels={erroredProviderLabels} importErrored={importMutation.isError} /> {visibleEntries.length > 0 ? ( {visibleEntries.map((entry) => ( ))} ) : null} {showEmptyState ? : null} ); } const styles = StyleSheet.create((theme) => ({ filterTriggerWrap: { paddingBottom: theme.spacing[2], }, filterTrigger: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1.5], alignSelf: "flex-start", paddingVertical: theme.spacing[1.5], paddingHorizontal: theme.spacing[3], borderRadius: theme.borderRadius.md, backgroundColor: theme.colors.surface1, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, }, filterTriggerHovered: { backgroundColor: theme.colors.surface2, }, filterTriggerPressed: { backgroundColor: theme.colors.surface3, }, filterTriggerText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, list: { gap: theme.spacing[1], }, row: { flexDirection: "row", alignItems: "flex-start", gap: theme.spacing[2], paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[2], marginHorizontal: -theme.spacing[2], borderRadius: theme.borderRadius.lg, }, rowHovered: { backgroundColor: theme.colors.surface1, }, rowPressed: { backgroundColor: theme.colors.surface2, }, rowIconWrap: { width: theme.iconSize.md, paddingTop: 2, alignItems: "center", justifyContent: "center", }, rowContent: { flex: 1, minWidth: 0, gap: theme.spacing[1], }, rowHeader: { flexDirection: "row", alignItems: "baseline", justifyContent: "space-between", gap: theme.spacing[2], }, rowTitle: { flex: 1, minWidth: 0, color: theme.colors.foreground, fontSize: theme.fontSize.base, }, rowMeta: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, rowPreview: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, lineHeight: 20, }, rowCwd: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, statusRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingVertical: theme.spacing[2], }, statusText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, emptyState: { alignItems: "center", justifyContent: "center", gap: theme.spacing[2], paddingVertical: theme.spacing[8], paddingHorizontal: theme.spacing[4], }, emptyStateIcon: { opacity: 0.6, marginBottom: theme.spacing[1], }, emptyStateTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.base, textAlign: "center", }, refreshButton: { padding: theme.spacing[2], marginRight: theme.spacing[1], borderRadius: theme.borderRadius.lg, }, refreshButtonPressed: { backgroundColor: theme.colors.surface2, }, refreshIconSlot: { width: 16, height: 16, alignItems: "center", justifyContent: "center", }, }));