import { useCallback, useMemo, useState, type ReactElement } from "react"; import { Pressable, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; import { useTranslation } from "react-i18next"; import { Archive, ChevronDown, ChevronRight, Unlink } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { getProviderIcon } from "@/components/provider-icons"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useIsCompactFormFactor, MAX_CONTENT_WIDTH } from "@/constants/layout"; import { isNative } from "@/constants/platform"; import { WorkspaceTabIcon, type WorkspaceTabPresentation, } from "@/screens/workspace/workspace-tab-presentation"; import type { Theme } from "@/styles/theme"; import type { SubagentRow } from "./select"; import { buildSubagentRowPresentationData, countFinishedSubagents, formatHeaderLabel, } from "./track-presentation"; const ThemedArchive = withUnistyles(Archive); const ThemedChevronDown = withUnistyles(ChevronDown); const ThemedChevronRight = withUnistyles(ChevronRight); const ThemedUnlink = withUnistyles(Unlink); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); export interface SubagentsTrackProps { rows: SubagentRow[]; onOpenSubagent: (id: string) => void; onOpenProviderSubagent: (parentAgentId: string, subagentId: string) => void; onArchiveSubagent: (id: string) => void; onArchiveFinished?: () => void; onDetachSubagent?: (id: string) => void; } const SUBAGENTS_LIST_MAX_HEIGHT = 200; function buildRowPresentation(row: SubagentRow): WorkspaceTabPresentation { const data = buildSubagentRowPresentationData(row); return { ...data, tooltip: data.label, modified: false, icon: getProviderIcon(row.provider), }; } export function SubagentsTrack({ rows, onOpenSubagent, onOpenProviderSubagent, onArchiveSubagent, onArchiveFinished, onDetachSubagent, }: SubagentsTrackProps): ReactElement | null { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const toggleExpanded = useCallback(() => { setExpanded((current) => !current); }, []); const surfaceStyle = useMemo( () => [styles.surface, expanded && styles.surfaceExpanded], [expanded], ); const headerStyle = useCallback( ({ hovered, pressed }: PressableStateCallbackType) => [ styles.headerToggle, (hovered || pressed) && styles.headerActive, ], [], ); const headerContainerStyle = useMemo( () => [styles.header, expanded ? styles.headerDivider : styles.headerCollapsed], [expanded], ); if (rows.length === 0) { return null; } const headerLabel = formatHeaderLabel(rows); const finishedCount = countFinishedSubagents(rows); return ( {expanded ? ( ) : ( )} {headerLabel} {finishedCount > 0 && onArchiveFinished ? ( ) : null} {expanded ? ( {rows.map((row) => ( ))} ) : null} ); } interface SubagentsTrackRowProps { row: SubagentRow; onOpenSubagent: (id: string) => void; onOpenProviderSubagent: (parentAgentId: string, subagentId: string) => void; onArchiveSubagent: (id: string) => void; onDetachSubagent?: (id: string) => void; } function SubagentsTrackRow({ row, onOpenSubagent, onOpenProviderSubagent, onArchiveSubagent, onDetachSubagent, }: SubagentsTrackRowProps): ReactElement { const { t } = useTranslation(); const isCompact = useIsCompactFormFactor(); const [hovered, setHovered] = useState(false); const presentation = useMemo(() => buildRowPresentation(row), [row]); const displayLabel = presentation.titleState === "loading" ? t("common.states.loading") : presentation.label; const handlePress = useCallback(() => { if (row.kind === "provider") { onOpenProviderSubagent(row.parentAgentId, row.id); } else { onOpenSubagent(row.id); } }, [onOpenProviderSubagent, onOpenSubagent, row]); const handleArchivePress = useCallback(() => { onArchiveSubagent(row.id); }, [onArchiveSubagent, row.id]); const handleDetachPress = useCallback(() => { onDetachSubagent?.(row.id); }, [onDetachSubagent, row.id]); const handlePointerEnter = useCallback(() => setHovered(true), []); const handlePointerLeave = useCallback(() => setHovered(false), []); const actionsAlwaysVisible = isNative || isCompact; const actionsVisible = actionsAlwaysVisible || hovered; return ( // Wrapper View handles hover so moving the pointer between the row and // the archive button doesn't drop the hover state — the same pattern // used by sidebar workspace rows. {({ pressed }) => ( {displayLabel} {row.kind === "paseo" ? ( ) : null} )} ); } function SubagentRowActions({ rowId, displayLabel, visible, onDetachPress, onArchivePress, }: { rowId: string; displayLabel: string; visible: boolean; onDetachPress?: () => void; onArchivePress: () => void; }): ReactElement { const { t } = useTranslation(); return ( {onDetachPress ? ( ) : null} ); } type SubagentActionIcon = "archive" | "detach"; function renderSubagentActionIcon(icon: SubagentActionIcon, isActive: boolean): ReactElement { const uniProps = isActive ? foregroundColorMapping : foregroundMutedColorMapping; if (icon === "detach") { return ; } return ; } function SubagentActionButton({ accessibilityLabel, testID, tooltipLabel, icon, visible, onPress, }: { accessibilityLabel: string; testID: string; tooltipLabel: string; icon: SubagentActionIcon; visible: boolean; onPress: () => void; }): ReactElement { return ( {({ hovered, pressed }) => renderSubagentActionIcon(icon, hovered || pressed)} {tooltipLabel} ); } const styles = StyleSheet.create((theme) => ({ outer: { width: "100%", alignItems: "center", paddingHorizontal: theme.spacing[4], }, track: { width: "100%", maxWidth: MAX_CONTENT_WIDTH, marginBottom: -theme.spacing[4], }, surface: { alignSelf: "stretch", backgroundColor: theme.colors.surface1, borderWidth: theme.borderWidth[1], borderColor: theme.colors.borderAccent, borderBottomWidth: 0, borderTopLeftRadius: theme.borderRadius["2xl"], borderTopRightRadius: theme.borderRadius["2xl"], overflow: "hidden", }, surfaceExpanded: { paddingBottom: theme.spacing[4], }, header: { flexDirection: "row", alignItems: "center", }, headerToggle: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingLeft: theme.spacing[3], paddingRight: theme.spacing[1], paddingVertical: theme.spacing[2], }, headerAction: { paddingRight: theme.spacing[2], }, headerCollapsed: { paddingBottom: theme.spacing[4], }, headerActive: { backgroundColor: theme.colors.surface2, }, headerDivider: { borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, headerLabel: { flexShrink: 1, minWidth: 0, fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, }, scroll: { maxHeight: SUBAGENTS_LIST_MAX_HEIGHT, }, scrollContent: { paddingVertical: theme.spacing[1], }, row: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, rowActive: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], backgroundColor: theme.colors.surface2, }, rowLabel: { flex: 1, minWidth: 0, fontSize: theme.fontSize.sm, color: theme.colors.foreground, }, actionClusterVisible: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], opacity: 1, }, actionClusterHidden: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], opacity: 0, }, actionButton: { padding: theme.spacing[1], alignItems: "center", justifyContent: "center", }, tooltipText: { fontSize: theme.fontSize.xs, color: theme.colors.foreground, }, }));