import { useCallback, useMemo, type ReactElement } from "react"; import { Pressable, Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import { Archive, Unlink } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { getProviderIcon } from "@/components/provider-icons"; import { ComposerTrackActions, ComposerTrackPill, ComposerTrackRow } from "@/composer/tracks"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useIsCompactFormFactor } 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 type { ArchiveFinishedStatus } from "./use-archive-finished"; import { buildSubagentPillPresentation, buildSubagentRowPresentationData, countFinishedSubagents, } from "./track-presentation"; const ThemedArchive = withUnistyles(Archive); 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; archiveFinishedStatus?: ArchiveFinishedStatus; onDetachSubagent?: (id: string) => void; } const IDLE_ARCHIVE_FINISHED_STATUS: ArchiveFinishedStatus = { kind: "idle" }; /** Leading and action glyphs share one size so rows keep a single icon column. */ const ROW_ICON_SIZE = 14; 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, archiveFinishedStatus = IDLE_ARCHIVE_FINISHED_STATUS, onDetachSubagent, }: SubagentsTrackProps): ReactElement | null { const { t } = useTranslation(); const isArchivingFinished = archiveFinishedStatus.kind === "archiving"; const isArchiveFinishedFailed = archiveFinishedStatus.kind === "failed"; if (rows.length === 0 && !isArchivingFinished && !isArchiveFinishedFailed) { return null; } const pill = buildSubagentPillPresentation(t, rows); const finishedCount = countFinishedSubagents(rows); const showArchiveFinished = finishedCount > 0 || isArchivingFinished || isArchiveFinishedFailed; return ( {showArchiveFinished && onArchiveFinished ? ( 0}> ) : null} {rows.map((row) => ( ))} ); } /** * Bulk archive, as a row above the list rather than an icon next to the count. The pill has no * header to hang an icon off, and a destructive-ish action reads better with its name attached. */ function ArchiveFinishedRow({ status, disabled, onPress, }: { status: ArchiveFinishedStatus; disabled: boolean; onPress: () => void; }): ReactElement { const { t } = useTranslation(); const renderRow = useCallback( ({ active }: { active: boolean }) => ( <> {t("subagents.archiveFinishedAction")} {status.kind === "archiving" ? ( {status.completedCount}/{status.totalCount} ) : null} {status.kind === "failed" ? ( {t("subagents.archiveFinishedRetry", { failed: status.failedCount, total: status.totalCount, })} ) : null} ), [status, t], ); return ( {renderRow} ); } 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 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 actionsAlwaysVisible = isNative || isCompact; const renderRow = useCallback( ({ active }: { active: boolean }) => ( <> {displayLabel} {presentation.subtitle ? ( {presentation.subtitle} ) : null} {row.kind === "paseo" ? ( ) : null} ), [ actionsAlwaysVisible, displayLabel, handleArchivePress, handleDetachPress, onDetachSubagent, presentation, row.kind, row.id, ], ); return ( {renderRow} ); } 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) => ({ // `flexBasis: "auto"` rather than `flex: 1`: a zero-basis label contributes nothing to the row's // intrinsic width, so the panel measures itself at its floor and truncates every label at once. rowLabel: { flexGrow: 1, flexShrink: 1, flexBasis: "auto", minWidth: 0, fontSize: theme.fontSize.base, color: theme.colors.foreground, }, // Trailing metadata — provider context on a subagent row, progress on the archive row. No width // cap: the panel's own ceiling bounds it. It shrinks twice as fast as the label, so a wordy // provider subtitle gives way first instead of squeezing the thing that names the row. rowTrailing: { flexShrink: 2, minWidth: 0, fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, 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.sm, color: theme.colors.foreground, }, }));