import { memo, useMemo, useState } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; 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, shouldDisplayMemberCurrentTask, } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, getMemberLaunchDiagnosticsErrorMessage, hasMemberLaunchDiagnosticsDetails, hasMemberLaunchDiagnosticsError, } from '@renderer/utils/memberLaunchDiagnostics'; import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState'; import { isLeadMember } from '@shared/utils/leadDetection'; import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState'; 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 { 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 mounts detailed member data only while open. */ export const MemberHoverCard = memo(function MemberHoverCard({ name, color, teamName, onOpenTask, children, }: MemberHoverCardProps): React.JSX.Element { const [open, setOpen] = useState(false); return ( {children} {open ? ( ) : null} ); }); interface MemberHoverCardContentProps { name: string; color?: string; teamName?: string; onOpenTask?: (task: TeamTaskWithKanban) => void; } const MemberHoverCardContent = ({ name, color, teamName, onOpenTask, }: MemberHoverCardContentProps): React.JSX.Element | null => { const { t } = useAppTranslation('team'); const { isLight } = useTheme(); const effectiveTeamName = useStore((s) => teamName ?? s.selectedTeamName); const { member, teamMembers, tasks, isTeamAlive, progress, memberSpawnSnapshot, memberSpawnStatuses, spawnEntry, runtimeRunId, runtimeEntries, 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, runtimeEntries: effectiveTeamName ? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members : undefined, runtimeEntry: effectiveTeamName ? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members[name] : undefined, leadActivity: effectiveTeamName ? s.leadActivityByTeam[effectiveTeamName] : undefined, })) ); const openMemberProfile = useStore((s) => s.openMemberProfile); const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); if (!member) { return null; } const launchJoinMilestones = getLaunchJoinMilestonesFromMembers({ members: teamMembers, memberSpawnStatuses, memberSpawnSnapshot, memberRuntimeEntries: runtimeEntries, }); const isLaunchSettling = progress?.state === 'ready' && getLaunchJoinState(launchJoinMilestones).hasMembersStillJoining; const colors = getTeamColorSet(color ?? member.color ?? ''); const roleLabel = formatAgentRole(member.role) ?? formatAgentRole(member.agentType); const currentTaskCandidate: TeamTaskWithKanban | null = member.currentTaskId ? (tasks.find((t) => t.id === member.currentTaskId) ?? null) : null; const currentTask = isDisplayableCurrentTask(currentTaskCandidate) && shouldDisplayMemberCurrentTask({ member, isTeamAlive, spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, spawnEntry, runtimeEntry, }) ? currentTaskCandidate : null; const presentationMember = member.currentTaskId && !currentTask ? { ...member, currentTaskId: null, } : member; const launchPresentation = buildMemberLaunchPresentation({ member: presentationMember, spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnLivenessSource: spawnEntry?.livenessSource, spawnRuntimeAlive: spawnEntry?.runtimeAlive, spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed, spawnBootstrapStalled: spawnEntry?.bootstrapStalled, spawnAgentToolAccepted: spawnEntry?.agentToolAccepted, spawnHardFailure: spawnEntry?.hardFailure, spawnHardFailureReason: spawnEntry?.hardFailureReason, spawnError: spawnEntry?.error, spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic, spawnLivenessKind: spawnEntry?.livenessKind, spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt, spawnUpdatedAt: spawnEntry?.updatedAt, 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 === 'starting_stale' || 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, member, spawnEntry, runtimeEntry, }); const launchErrorMessage = getMemberLaunchDiagnosticsErrorMessage(launchDiagnosticsPayload); const showCopyDiagnostics = hasMemberLaunchDiagnosticsError(launchDiagnosticsPayload) && hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload); const reviewTaskCandidate: TeamTaskWithKanban | null = tasks ? (tasks.find( (task) => task.reviewer === member.name && task.id !== currentTask?.id && getTeamTaskWorkflowColumn(task) === 'review' ) ?? null) : null; const reviewTask = reviewTaskCandidate && shouldDisplayMemberCurrentTask({ member, isTeamAlive, spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, spawnEntry, runtimeEntry, }) ? reviewTaskCandidate : null; return (
{/* 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}
); };