import { memo } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card'; import { getTeamColorSet, getThemedBadge, getThemedBorder, getThemedText, } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { useStore } from '@renderer/store'; import { getCurrentProvisioningProgressForTeam, selectResolvedMemberForTeamName, selectTeamIsAliveForName, selectTeamMemberSnapshotsForName, selectTeamTasksForName, } from '@renderer/store/slices/teamSlice'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { agentAvatarUrl, buildMemberAvatarMap, buildMemberLaunchPresentation, displayMemberName, } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, getMemberLaunchDiagnosticsErrorMessage, hasMemberLaunchDiagnosticsDetails, hasMemberLaunchDiagnosticsError, } from '@renderer/utils/memberLaunchDiagnostics'; import { isLeadMember } from '@shared/utils/leadDetection'; import { ExternalLink } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from '../provisioningSteps'; import { CurrentTaskIndicator } from './CurrentTaskIndicator'; import { MemberLaunchDiagnosticsButton } from './MemberLaunchDiagnosticsButton'; import { MemberPresenceDot } from './MemberPresenceDot'; import type { LeadActivityState, TeamTaskWithKanban } from '@shared/types'; interface MemberHoverCardProps { /** The member name to look up */ name: string; /** Color key for the member */ color?: string; /** Owning team context for store lookups. */ teamName?: string; /** Called when user clicks on the current task */ onOpenTask?: (task: TeamTaskWithKanban) => void; children: React.ReactNode; } /** * Wraps children in a HoverCard that shows member info on hover. * Reads member data from the team snapshot + resolved member selectors. * Falls back to a simple wrapper when member data is unavailable. */ export const MemberHoverCard = memo(function MemberHoverCard({ name, color, teamName, onOpenTask, children, }: MemberHoverCardProps): React.JSX.Element { const { isLight } = useTheme(); const selectedTeamName = useStore((s) => s.selectedTeamName); const effectiveTeamName = teamName ?? selectedTeamName; const { member, teamMembers, tasks, isTeamAlive, progress, memberSpawnSnapshot, memberSpawnStatuses, spawnEntry, runtimeRunId, runtimeEntry, leadActivity, } = useStore( useShallow((s) => ({ member: effectiveTeamName ? selectResolvedMemberForTeamName(s, effectiveTeamName, name) : null, teamMembers: effectiveTeamName ? selectTeamMemberSnapshotsForName(s, effectiveTeamName) : [], tasks: effectiveTeamName ? selectTeamTasksForName(s, effectiveTeamName) : [], isTeamAlive: effectiveTeamName ? selectTeamIsAliveForName(s, effectiveTeamName) : undefined, progress: effectiveTeamName ? getCurrentProvisioningProgressForTeam(s, effectiveTeamName) : null, memberSpawnSnapshot: effectiveTeamName ? s.memberSpawnSnapshotsByTeam[effectiveTeamName] : undefined, memberSpawnStatuses: effectiveTeamName ? s.memberSpawnStatusesByTeam[effectiveTeamName] : undefined, spawnEntry: effectiveTeamName ? s.memberSpawnStatusesByTeam[effectiveTeamName]?.[name] : undefined, runtimeRunId: effectiveTeamName ? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.runId : undefined, runtimeEntry: effectiveTeamName ? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members[name] : undefined, leadActivity: effectiveTeamName ? s.leadActivityByTeam[effectiveTeamName] : undefined, })) ); const openMemberProfile = useStore((s) => s.openMemberProfile); const avatarMap = buildMemberAvatarMap(teamMembers); if (!member) { return <>{children}; } const launchJoinMilestones = getLaunchJoinMilestonesFromMembers({ members: teamMembers, memberSpawnStatuses, memberSpawnSnapshot, }); const isLaunchSettling = progress?.state === 'ready' && getLaunchJoinState(launchJoinMilestones).hasMembersStillJoining; const colors = getTeamColorSet(color ?? member.color ?? ''); const roleLabel = formatAgentRole(member.role) ?? formatAgentRole(member.agentType); const launchPresentation = buildMemberLaunchPresentation({ member, spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnLivenessSource: spawnEntry?.livenessSource, spawnRuntimeAlive: spawnEntry?.runtimeAlive, spawnBootstrapStalled: spawnEntry?.bootstrapStalled, runtimeEntry, runtimeAdvisory: member.runtimeAdvisory, isLaunchSettling, isTeamAlive, isTeamProvisioning: false, leadActivity: isLeadMember(member) ? leadActivity : undefined, }); const presenceLabel = launchPresentation.presenceLabel; const launchVisualState = launchPresentation.launchVisualState; const launchStatusLabel = launchPresentation.launchStatusLabel; const dotClass = launchPresentation.dotClass; const runtimeAdvisoryLabel = launchPresentation.runtimeAdvisoryLabel; const runtimeAdvisoryTitle = launchPresentation.runtimeAdvisoryTitle; const runtimeAdvisoryTone = launchPresentation.runtimeAdvisoryTone; const badgeLabel = runtimeAdvisoryTone === 'error' && runtimeAdvisoryLabel ? runtimeAdvisoryLabel : launchVisualState === 'bootstrap_stalled' || launchVisualState === 'runtime_pending' || launchVisualState === 'permission_pending' || launchVisualState === 'shell_only' || launchVisualState === 'runtime_candidate' || launchVisualState === 'registered_only' || launchVisualState === 'stale_runtime' ? (launchStatusLabel ?? presenceLabel) : presenceLabel; const launchDiagnosticsPayload = buildMemberLaunchDiagnosticsPayload({ teamName: effectiveTeamName, runId: runtimeRunId ?? memberSpawnSnapshot?.runId ?? progress?.runId, memberName: member.name, spawnEntry, runtimeEntry, }); const launchErrorMessage = getMemberLaunchDiagnosticsErrorMessage(launchDiagnosticsPayload); const showCopyDiagnostics = hasMemberLaunchDiagnosticsError(launchDiagnosticsPayload) && hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload); const currentTask: TeamTaskWithKanban | null = member.currentTaskId ? (tasks.find((t) => t.id === member.currentTaskId) ?? null) : null; const reviewTask: TeamTaskWithKanban | null = tasks ? (tasks.find( (task) => task.reviewer === member.name && task.id !== member.currentTaskId && (task.reviewState === 'review' || task.kanbanColumn === 'review') ) ?? null) : null; return ( {children}
{/* Header: avatar + name + presence */}
{member.name}
{displayMemberName(member.name)} {badgeLabel}
{roleLabel && ( {roleLabel} )}
{/* Current task */} {currentTask && (
onOpenTask(currentTask) : undefined} />
)} {/* Review task */} {reviewTask && (
onOpenTask(reviewTask) : undefined} />
)} {launchErrorMessage ? (
{launchErrorMessage} {showCopyDiagnostics ? ( ) : null}
) : null}
); });