import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem, } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useCallback, useMemo, useState, type ReactElement } from 'react' import { router } from 'expo-router' import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles' import { formatTimeAgo } from '@/utils/time' import { shortenPath } from '@/utils/shorten-path' import { type AggregatedAgent } from '@/hooks/use-aggregated-agents' import { useSessionStore } from '@/stores/session-store' import { AgentStatusDot } from '@/components/agent-status-dot' import { prepareWorkspaceTab } from '@/utils/workspace-navigation' interface AgentListProps { agents: AggregatedAgent[] showCheckoutInfo?: boolean isRefreshing?: boolean onRefresh?: () => void selectedAgentId?: string onAgentSelect?: () => void listFooterComponent?: ReactElement | null showAttentionIndicator?: boolean } interface AgentListSection { key: string title: string data: AggregatedAgent[] } function deriveDateSectionLabel(lastActivityAt: Date): string { const now = new Date() const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000) const activityStart = new Date( lastActivityAt.getFullYear(), lastActivityAt.getMonth(), lastActivityAt.getDate() ) if (activityStart.getTime() >= todayStart.getTime()) { return 'Today' } if (activityStart.getTime() >= yesterdayStart.getTime()) { return 'Yesterday' } const diffTime = todayStart.getTime() - activityStart.getTime() const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)) if (diffDays <= 7) { return 'This week' } if (diffDays <= 30) { return 'This month' } return 'Older' } function formatStatusLabel(status: AggregatedAgent['status']): string { switch (status) { case 'initializing': return 'Starting' case 'idle': return 'Idle' case 'running': return 'Running' case 'error': return 'Error' case 'closed': return 'Closed' default: return status } } function SessionBadge({ label, tone = 'neutral', }: { label: string tone?: 'neutral' | 'warning' | 'danger' }) { return ( {label} ) } function SessionRow({ agent, isMobile, selectedAgentId, showAttentionIndicator, onPress, onLongPress, }: { agent: AggregatedAgent isMobile: boolean selectedAgentId?: string showAttentionIndicator: boolean onPress: (agent: AggregatedAgent) => void onLongPress: (agent: AggregatedAgent) => void }) { const timeAgo = formatTimeAgo(agent.lastActivityAt) const agentKey = `${agent.serverId}:${agent.id}` const isSelected = selectedAgentId === agentKey const statusLabel = formatStatusLabel(agent.status) const projectPath = shortenPath(agent.cwd) return ( [ styles.row, isSelected && styles.rowSelected, hovered && styles.rowHovered, pressed && styles.rowPressed, ]} onPress={() => onPress(agent)} onLongPress={() => onLongPress(agent)} testID={`agent-row-${agent.serverId}-${agent.id}`} > {agent.title || 'New session'} {agent.archivedAt ? : null} {(agent.pendingPermissionCount ?? 0) > 0 ? ( ) : null} {!isMobile && showAttentionIndicator && agent.requiresAttention ? ( ) : null} {isMobile && ( {projectPath} · {statusLabel} · {timeAgo} {agent.serverLabel ? ( <> · {agent.serverLabel} ) : null} )} {!isMobile && ( <> {projectPath} {statusLabel} {timeAgo} )} {isMobile && showAttentionIndicator && agent.requiresAttention ? ( ) : null} ) } function SessionTableSection({ section, isMobile, selectedAgentId, showAttentionIndicator, onAgentPress, onAgentLongPress, }: { section: AgentListSection isMobile: boolean selectedAgentId?: string showAttentionIndicator: boolean onAgentPress: (agent: AggregatedAgent) => void onAgentLongPress: (agent: AggregatedAgent) => void }) { return ( {section.title} {section.data.map((agent, index) => ( 0 ? styles.rowDivider : undefined} > ))} ) } export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAgentId, onAgentSelect, listFooterComponent, showAttentionIndicator = true, }: AgentListProps) { const { theme } = useUnistyles() const insets = useSafeAreaInsets() const [actionAgent, setActionAgent] = useState(null) const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm' const actionClient = useSessionStore((state) => actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null ) const isActionSheetVisible = actionAgent !== null const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionClient) const handleAgentPress = useCallback( (agent: AggregatedAgent) => { if (isActionSheetVisible) { return } const serverId = agent.serverId const agentId = agent.id onAgentSelect?.() const route = prepareWorkspaceTab({ serverId, workspaceId: agent.cwd, target: { kind: 'agent', agentId }, }) router.navigate(route as any) }, [isActionSheetVisible, onAgentSelect] ) const handleAgentLongPress = useCallback((agent: AggregatedAgent) => { setActionAgent(agent) }, []) const handleCloseActionSheet = useCallback(() => { setActionAgent(null) }, []) const handleArchiveAgent = useCallback(() => { if (!actionAgent || !actionClient) { return } void actionClient.archiveAgent(actionAgent.id) setActionAgent(null) }, [actionAgent, actionClient]) const sections = useMemo((): AgentListSection[] => { const order = ['Today', 'Yesterday', 'This week', 'This month', 'Older'] as const const buckets = new Map() for (const agent of agents) { const label = deriveDateSectionLabel(agent.lastActivityAt) const existing = buckets.get(label) ?? [] existing.push(agent) buckets.set(label, existing) } const result: AgentListSection[] = [] for (const label of order) { const data = buckets.get(label) if (!data || data.length === 0) { continue } result.push({ key: `date:${label}`, title: label, data }) } return result }, [agents]) const renderSection: ListRenderItem = useCallback( ({ item: section }) => ( ), [handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId, showAttentionIndicator] ) const keyExtractor = useCallback((section: AgentListSection) => section.key, []) return ( <> ) : undefined } /> {isActionDaemonUnavailable ? 'Host offline' : 'Archive this session?'} Cancel Archive ) } const styles = StyleSheet.create((theme) => ({ list: { flex: 1, minHeight: 0, }, listContent: { paddingHorizontal: { xs: theme.spacing[3], md: theme.spacing[6], }, paddingTop: theme.spacing[2], paddingBottom: theme.spacing[6], gap: theme.spacing[1], }, sectionBlock: { marginTop: theme.spacing[2], }, sectionHeading: { flexDirection: 'row', alignItems: 'center', gap: theme.spacing[3], paddingHorizontal: theme.spacing[1], marginBottom: theme.spacing[2], }, sectionTitle: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, color: theme.colors.foregroundMuted, }, listCard: { overflow: { xs: 'hidden' as const, md: 'visible' as const, }, borderRadius: { xs: theme.borderRadius.lg, md: 0, }, }, rowDivider: { borderTopWidth: { xs: StyleSheet.hairlineWidth, md: 0, }, borderTopColor: theme.colors.border, }, row: { flexDirection: 'row', alignItems: 'center', paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], borderRadius: { xs: theme.borderRadius.lg, md: 0, }, marginBottom: { xs: theme.spacing[1], md: 0, }, }, rowLeading: { marginRight: theme.spacing[3], }, rowContent: { flex: 1, minWidth: 0, }, rowTitleRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: theme.spacing[2], }, rowMetaRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: theme.spacing[1], marginTop: 2, }, rowTrailing: { marginLeft: theme.spacing[2], }, rowSelected: { backgroundColor: theme.colors.surface2, }, rowHovered: { backgroundColor: theme.colors.surface1, }, rowPressed: { backgroundColor: theme.colors.surface2, }, sessionTitle: { flexShrink: 1, fontSize: theme.fontSize.sm, fontWeight: '500', color: theme.colors.foreground, opacity: 0.86, }, sessionTitleHighlighted: { opacity: 1, }, sessionMetaText: { maxWidth: '100%', fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, sessionMetaSeparator: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, opacity: 0.7, }, columnMeta: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, flexShrink: 1, minWidth: 60, maxWidth: 200, marginLeft: theme.spacing[4], }, columnMetaFixed: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, flexShrink: 0, width: 72, textAlign: 'right' as const, }, badge: { paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.surface2, }, badgeWarning: { backgroundColor: 'rgba(245, 158, 11, 0.12)', }, badgeDanger: { backgroundColor: 'rgba(239, 68, 68, 0.14)', }, badgeText: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.medium, color: theme.colors.foregroundMuted, }, badgeTextWarning: { color: theme.colors.palette.amber[500], }, badgeTextDanger: { color: theme.colors.palette.red[300], }, sheetOverlay: { flex: 1, justifyContent: 'flex-end', }, sheetBackdrop: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(0,0,0,0.35)', }, sheetContainer: { backgroundColor: theme.colors.surface2, borderTopLeftRadius: theme.borderRadius['2xl'], borderTopRightRadius: theme.borderRadius['2xl'], paddingHorizontal: theme.spacing[6], paddingTop: theme.spacing[4], gap: theme.spacing[4], }, sheetHandle: { alignSelf: 'center', width: 40, height: 4, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.foregroundMuted, opacity: 0.3, }, sheetTitle: { fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, textAlign: 'center', }, sheetButtonRow: { flexDirection: 'row', gap: theme.spacing[3], }, sheetButton: { flex: 1, borderRadius: theme.borderRadius.lg, paddingVertical: theme.spacing[4], alignItems: 'center', justifyContent: 'center', }, sheetArchiveButton: { backgroundColor: theme.colors.primary, }, sheetArchiveText: { color: theme.colors.primaryForeground, fontWeight: theme.fontWeight.semibold, fontSize: theme.fontSize.base, }, sheetArchiveTextDisabled: { opacity: 0.5, }, sheetCancelButton: { backgroundColor: theme.colors.surface1, }, sheetCancelText: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, fontSize: theme.fontSize.base, }, }))