import { useCallback, useEffect, useRef, useState } from 'react'; import { formatTeamModelSummary, getTeamEffortLabel, getTeamModelLabel, getTeamProviderLabel, } from '@renderer/components/team/dialogs/TeamModelSelector'; import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection'; import { MemberCard } from './MemberCard'; import type { TaskStatusCounts } from '@renderer/utils/pathNormalize'; import type { LeadActivityState, MemberSpawnStatusEntry, ResolvedTeamMember, TeamTaskWithKanban, } from '@shared/types'; interface MemberListProps { members: ResolvedTeamMember[]; memberTaskCounts?: Map; taskMap?: Map; pendingRepliesByMember?: Record; memberSpawnStatuses?: Map; isTeamAlive?: boolean; isTeamProvisioning?: boolean; leadActivity?: LeadActivityState; launchParams?: TeamLaunchParams; onMemberClick?: (member: ResolvedTeamMember) => void; onSendMessage?: (member: ResolvedTeamMember) => void; onAssignTask?: (member: ResolvedTeamMember) => void; onOpenTask?: (task: TeamTaskWithKanban) => void; } export const MemberList = ({ members, memberTaskCounts, taskMap, pendingRepliesByMember, memberSpawnStatuses, isTeamAlive, isTeamProvisioning, leadActivity, launchParams, onMemberClick, onSendMessage, onAssignTask, onOpenTask, }: MemberListProps): React.JSX.Element => { const containerRef = useRef(null); const [isWide, setIsWide] = useState(false); const handleResize = useCallback((entries: ResizeObserverEntry[]) => { const entry = entries[0]; if (entry) { setIsWide(entry.contentRect.width > 1000); } }, []); useEffect(() => { const el = containerRef.current; if (!el) return; const observer = new ResizeObserver(handleResize); observer.observe(el); return () => observer.disconnect(); }, [handleResize]); const gridClass = isWide ? 'grid grid-cols-2 gap-1' : 'grid grid-cols-1 gap-1'; const activeMembers = members .filter((m) => !m.removedAt) .sort((a, b) => { if (isLeadMember(a)) return -1; if (isLeadMember(b)) return 1; return 0; }); const removedMembers = members.filter((m) => m.removedAt); const colorMap = buildMemberColorMap(members); const buildRuntimeSummary = useCallback( (member: ResolvedTeamMember): string | undefined => { const effectiveProvider = member.providerId ?? launchParams?.providerId ?? 'anthropic'; const effectiveModel = member.model?.trim() || launchParams?.model?.trim() || ''; const effectiveEffort = member.effort ?? launchParams?.effort; return formatTeamModelSummary(effectiveProvider, effectiveModel, effectiveEffort); }, [launchParams] ); if (members.length === 0) { return (
Solo team — lead only
); } const renderCard = (member: ResolvedTeamMember, isRemoved: boolean): React.JSX.Element => { const currentTask = member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null; const reviewTask = taskMap ? (Array.from(taskMap.values()).find( (task) => task.reviewer === member.name && task.id !== member.currentTaskId && (task.reviewState === 'review' || task.kanbanColumn === 'review') ) ?? null) : null; const awaitingReply = Boolean(pendingRepliesByMember?.[member.name]); const spawnEntry = memberSpawnStatuses?.get(member.name); return ( onOpenTask?.(currentTask) : undefined} onOpenReviewTask={!isRemoved && reviewTask ? () => onOpenTask?.(reviewTask) : undefined} onClick={() => onMemberClick?.(member)} onSendMessage={() => onSendMessage?.(member)} onAssignTask={() => onAssignTask?.(member)} /> ); }; return (
{activeMembers.map((member) => renderCard(member, false))}
{removedMembers.length > 0 && ( <>
Removed ({removedMembers.length})
{removedMembers.map((member) => renderCard(member, true))}
)}
); };