import { useMemo } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { useStore } from '@renderer/store'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { agentAvatarUrl, buildMemberAvatarMap, buildMemberLaunchPresentation, displayMemberName, } from '@renderer/utils/memberHelpers'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; import { AlertTriangle, GitBranch, Loader2, MessageSquare, Plus } from 'lucide-react'; import { CurrentTaskIndicator } from './CurrentTaskIndicator'; import type { TaskStatusCounts } from '@renderer/utils/pathNormalize'; import type { LeadActivityState, MemberLaunchState, MemberSpawnLivenessSource, MemberSpawnStatus, ResolvedTeamMember, TeamTaskWithKanban, } from '@shared/types'; interface MemberCardProps { member: ResolvedTeamMember; memberColor: string; runtimeSummary?: string; taskCounts?: TaskStatusCounts | null; isTeamAlive?: boolean; isTeamProvisioning?: boolean; leadActivity?: LeadActivityState; currentTask?: TeamTaskWithKanban | null; reviewTask?: TeamTaskWithKanban | null; isAwaitingReply?: boolean; isRemoved?: boolean; spawnStatus?: MemberSpawnStatus; spawnError?: string; spawnLivenessSource?: MemberSpawnLivenessSource; spawnLaunchState?: MemberLaunchState; spawnRuntimeAlive?: boolean; isLaunchSettling?: boolean; onOpenTask?: () => void; onOpenReviewTask?: () => void; onClick?: () => void; onSendMessage?: () => void; onAssignTask?: () => void; } function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): { summary: string | undefined; memory: string | undefined; } { const trimmed = runtimeSummary?.trim(); if (!trimmed) { return { summary: undefined, memory: undefined }; } const match = /^(.*?)(?:\s·\s(\d+(?:\.\d+)?\s(?:B|KB|MB|GB|TB)))$/.exec(trimmed); if (!match) { return { summary: trimmed, memory: undefined }; } return { summary: match[1]?.trim() || undefined, memory: match[2]?.trim() || undefined, }; } export const MemberCard = ({ member, memberColor, runtimeSummary, taskCounts, isTeamAlive, isTeamProvisioning, leadActivity, currentTask, reviewTask, isAwaitingReply, isRemoved, spawnStatus, spawnError, spawnLivenessSource, spawnLaunchState, spawnRuntimeAlive, isLaunchSettling, onOpenTask, onOpenReviewTask, onClick, onSendMessage, onAssignTask, }: MemberCardProps): React.JSX.Element => { // NOTE: lead context display disabled — usage formula is inaccurate // const teamName = useStore((s) => s.selectedTeamName); // const leadContext = useStore((s) => // member.agentType === 'team-lead' && teamName ? s.leadContextByTeam[teamName] : undefined // ); const selectedTeamName = useStore((s) => s.selectedTeamName); const teamMembers = useStore((s) => selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] ); const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); const launchPresentation = buildMemberLaunchPresentation({ member, spawnStatus, spawnLaunchState, spawnLivenessSource, spawnRuntimeAlive, runtimeAdvisory: member.runtimeAdvisory, isLaunchSettling, isTeamAlive, isTeamProvisioning, leadActivity, }); const dotClass = launchPresentation.dotClass; const runtimeAdvisoryLabel = launchPresentation.runtimeAdvisoryLabel; const runtimeAdvisoryTitle = launchPresentation.runtimeAdvisoryTitle; const runtimeAdvisoryTone = launchPresentation.runtimeAdvisoryTone; const presenceLabel = launchPresentation.presenceLabel; const spawnCardClass = launchPresentation.cardClass; const launchVisualState = launchPresentation.launchVisualState; const launchStatusLabel = launchPresentation.launchStatusLabel; const displayPresenceLabel = launchVisualState === 'runtime_pending' || launchVisualState === 'permission_pending' ? (launchStatusLabel ?? presenceLabel) : presenceLabel; const colors = getTeamColorSet(memberColor); const { isLight } = useTheme(); const pending = taskCounts?.pending ?? 0; const inProgress = taskCounts?.inProgress ?? 0; const completed = taskCounts?.completed ?? 0; const totalTasks = pending + inProgress + completed; const progressPercent = totalTasks > 0 ? Math.round((completed / totalTasks) * 100) : 0; const roleLabel = formatAgentRole(member.role) ?? formatAgentRole(member.agentType); const { summary: runtimeSummaryText, memory: memoryLabel } = splitRuntimeSummaryMemory(runtimeSummary); const activityTask = currentTask ?? reviewTask ?? null; const activityTitle = currentTask ? `Current task: #${deriveTaskDisplayId(currentTask.id)}` : reviewTask ? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}` : undefined; const showStartingSkeleton = !isRemoved && presenceLabel === 'starting' && spawnLaunchState !== 'failed_to_start' && !activityTask && !runtimeSummary; const showLaunchBadge = !isRemoved && !activityTask && !runtimeAdvisoryLabel && (presenceLabel === 'starting' || presenceLabel === 'connecting' || launchVisualState === 'runtime_pending'); const launchBadgeLabel = presenceLabel === 'starting' ? presenceLabel : displayPresenceLabel; const showRuntimeAdvisoryBadge = !isRemoved && Boolean(runtimeAdvisoryLabel) && !showLaunchBadge && spawnStatus !== 'error' && (Boolean(activityTask) || !isAwaitingReply); return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } }} >
{member.name}
{displayMemberName(member.name)} {member.gitBranch ? ( {member.gitBranch} ) : null} {currentTask ? ( ) : null} {reviewTask ? ( ) : null} {!activityTask && isAwaitingReply ? ( <> {runtimeAdvisoryTone === 'error' ? ( ) : ( )} {runtimeAdvisoryLabel ?? 'awaiting reply'} ) : null}
{showStartingSkeleton ? (