import { memo, useMemo, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { renderLinkifiedText } from '@renderer/utils/linkifiedText'; import { agentAvatarUrl, buildMemberAvatarMap, buildMemberLaunchPresentation, displayMemberName, isOpenCodeRelaunchActionable, } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, hasMemberLaunchDiagnosticsDetails, hasMemberLaunchDiagnosticsError, normalizeMemberLaunchFailureReason, } from '@renderer/utils/memberLaunchDiagnostics'; import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary'; import { isLeadMember } from '@shared/utils/leadDetection'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; import { AlertTriangle, Ban, GitBranch, MessageSquare, Plus, RotateCcw } from 'lucide-react'; import { CurrentTaskIndicator } from './CurrentTaskIndicator'; import { MemberLaunchDiagnosticsButton } from './MemberLaunchDiagnosticsButton'; import { MemberPresenceDot } from './MemberPresenceDot'; import type { MemberActivityTimerAnchor } from '@renderer/utils/memberActivityTimer'; import type { TaskStatusCounts } from '@renderer/utils/pathNormalize'; import type { LeadActivityState, MemberLaunchState, MemberSpawnLivenessSource, MemberSpawnStatus, MemberSpawnStatusEntry, ResolvedTeamMember, TeamAgentRuntimeEntry, TeamTaskWithKanban, } from '@shared/types'; interface MemberCardProps { member: ResolvedTeamMember; memberColor: string; fullBleedSurface?: boolean; runtimeSummary?: string; runtimeEntry?: TeamAgentRuntimeEntry; runtimeRunId?: string | null; taskCounts?: TaskStatusCounts | null; isTeamAlive?: boolean; isTeamProvisioning?: boolean; leadActivity?: LeadActivityState; currentTask?: TeamTaskWithKanban | null; reviewTask?: TeamTaskWithKanban | null; currentTaskTimer?: MemberActivityTimerAnchor | null; reviewTaskTimer?: MemberActivityTimerAnchor | null; currentTaskTimerRunning?: boolean; reviewTaskTimerRunning?: boolean; isAwaitingReply?: boolean; isRemoved?: boolean; spawnStatus?: MemberSpawnStatus; spawnEntry?: MemberSpawnStatusEntry; spawnError?: string; spawnLivenessSource?: MemberSpawnLivenessSource; spawnLaunchState?: MemberLaunchState; spawnRuntimeAlive?: boolean; isLaunchSettling?: boolean; onOpenTask?: () => void; onOpenReviewTask?: () => void; onClick?: () => void; onSendMessage?: () => void; onAssignTask?: () => void; onRestartMember?: (memberName: string) => Promise | void; onSkipMemberForLaunch?: (memberName: string) => Promise | void; } const MEMBER_ROW_SURFACE_BLEED_CLASS = '-mx-[calc(1rem-5px)] px-[calc(1rem-5px)]'; 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, }; } function getLaunchFailureLinkLabel(url: string): string { try { const parsed = new URL(url); if (parsed.hostname === 'openrouter.ai' && parsed.pathname === '/settings/credits') { return 'OpenRouter credits'; } } catch { return url; } return url; } export const MemberCard = memo(function MemberCard({ member, memberColor, fullBleedSurface = true, runtimeSummary, runtimeEntry, runtimeRunId, taskCounts, isTeamAlive, isTeamProvisioning, leadActivity, currentTask, reviewTask, currentTaskTimer, reviewTaskTimer, currentTaskTimerRunning = isTeamAlive !== false, reviewTaskTimerRunning = isTeamAlive !== false, isAwaitingReply, isRemoved, spawnStatus, spawnEntry, spawnError, spawnLivenessSource, spawnLaunchState, spawnRuntimeAlive, isLaunchSettling, onOpenTask, onOpenReviewTask, onClick, onSendMessage, onAssignTask, onRestartMember, onSkipMemberForLaunch, }: 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 [retryingLaunch, setRetryingLaunch] = useState(false); const [retryLaunchError, setRetryLaunchError] = useState(null); const [skippingLaunch, setSkippingLaunch] = useState(false); const [skipLaunchError, setSkipLaunchError] = useState(null); const teamMembers = useStore((s) => selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] ); const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); const presentationMember = member.currentTaskId && !currentTask ? { ...member, currentTaskId: null, } : member; const launchPresentation = buildMemberLaunchPresentation({ member: presentationMember, spawnStatus, spawnLaunchState, spawnLivenessSource, spawnRuntimeAlive, spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed, spawnBootstrapStalled: spawnEntry?.bootstrapStalled, spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt, spawnUpdatedAt: spawnEntry?.updatedAt, runtimeEntry, 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 === 'queued' || 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 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 memorySourceLabel = getRuntimeMemorySourceLabel(runtimeEntry); const isLead = isLeadMember(member); const workspacePath = member.cwd?.trim(); const showWorkspaceBadge = !isLead && !isRemoved && member.isolation === 'worktree'; const workspaceTooltipLines = [ 'Worktree isolation is enabled.', workspacePath ? `Path: ${workspacePath}` : 'Path is not available yet.', member.gitBranch ? `Branch: ${member.gitBranch}` : null, ].filter((line): line is string => Boolean(line)); 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 usesLaunchSkeletonSurface = spawnCardClass.includes('member-waiting-shimmer'); const rowSurfaceBleedClass = fullBleedSurface ? MEMBER_ROW_SURFACE_BLEED_CLASS : undefined; const showLaunchBadge = !isRemoved && !runtimeAdvisoryLabel && (presenceLabel === 'starting' || presenceLabel === 'connecting' || launchVisualState === 'queued' || launchVisualState === 'starting_stale' || launchVisualState === 'runtime_pending' || launchVisualState === 'shell_only' || launchVisualState === 'runtime_candidate' || launchVisualState === 'registered_only' || launchVisualState === 'stale_runtime'); const launchBadgeLabel = presenceLabel === 'starting' ? presenceLabel : displayPresenceLabel; const launchDiagnosticsPayload = useMemo( () => buildMemberLaunchDiagnosticsPayload({ teamName: selectedTeamName, runId: runtimeRunId, memberName: member.name, spawnStatus, launchState: spawnLaunchState, livenessSource: spawnLivenessSource, spawnEntry, runtimeEntry, }), [ member.name, runtimeEntry, runtimeRunId, selectedTeamName, spawnEntry, spawnLaunchState, spawnLivenessSource, spawnStatus, ] ); const showCopyDiagnostics = !isRemoved && hasMemberLaunchDiagnosticsError(launchDiagnosticsPayload) && hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload); const isFailedLaunch = spawnStatus === 'error' || spawnLaunchState === 'failed_to_start'; const isSkippedLaunch = spawnStatus === 'skipped' || spawnLaunchState === 'skipped_for_launch' || spawnEntry?.skippedForLaunch === true; const showFailedLaunchBadge = !isRemoved && isFailedLaunch; const showSkippedLaunchBadge = !isRemoved && isSkippedLaunch; const rawLaunchFailureReason = spawnError ?? spawnEntry?.hardFailureReason ?? spawnEntry?.runtimeDiagnostic ?? spawnEntry?.error; const launchFailureReason = showFailedLaunchBadge ? normalizeMemberLaunchFailureReason(rawLaunchFailureReason) : null; const hasLiveLaunchControls = isTeamAlive === true || isTeamProvisioning === true || isLaunchSettling === true; const hasRestartMemberControl = !isRemoved && !isLeadMember(member) && Boolean(onRestartMember) && hasLiveLaunchControls && runtimeEntry?.restartable !== false; const openCodeRelaunchActionable = isOpenCodeRelaunchActionable({ member, spawnEntry, runtimeEntry, }); const canRelaunchOpenCode = hasRestartMemberControl && openCodeRelaunchActionable; const canRetryLaunch = (showFailedLaunchBadge || showSkippedLaunchBadge) && hasRestartMemberControl; const canSkipFailedLaunch = showFailedLaunchBadge && !isLeadMember(member) && Boolean(onSkipMemberForLaunch) && hasLiveLaunchControls; const showRuntimeAdvisoryBadge = !isRemoved && Boolean(runtimeAdvisoryLabel) && !showLaunchBadge && !isFailedLaunch && !isSkippedLaunch && (Boolean(activityTask) || !isAwaitingReply); const restartActionIdleLabel = canRelaunchOpenCode ? 'Relaunch OpenCode' : 'Retry teammate'; const restartActionBusyLabel = canRelaunchOpenCode ? 'Relaunching OpenCode teammate' : 'Retrying teammate'; const restartActionErrorFallback = canRelaunchOpenCode ? 'Failed to relaunch OpenCode teammate' : 'Failed to retry teammate'; const handleRestartMember = async (event: React.MouseEvent): Promise => { event.preventDefault(); event.stopPropagation(); if (!onRestartMember || retryingLaunch) { return; } setRetryLaunchError(null); setRetryingLaunch(true); try { await onRestartMember(member.name); } catch (error) { setRetryLaunchError(error instanceof Error ? error.message : restartActionErrorFallback); } finally { setRetryingLaunch(false); } }; const handleSkipFailedLaunch = async ( event: React.MouseEvent ): Promise => { event.preventDefault(); event.stopPropagation(); if (!onSkipMemberForLaunch || skippingLaunch) { return; } setSkipLaunchError(null); setSkippingLaunch(true); try { await onSkipMemberForLaunch(member.name); } catch (error) { setSkipLaunchError(error instanceof Error ? error.message : 'Failed to skip teammate'); } finally { setSkippingLaunch(false); } }; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } }} >
{member.name}
{displayMemberName(member.name)} {member.gitBranch && !showWorkspaceBadge ? ( {member.gitBranch} ) : null} {showWorkspaceBadge ? ( worktree
{workspaceTooltipLines.map((line) => (

{line}

))}
) : null} {currentTask ? ( ) : null} {reviewTask ? ( ) : null} {!activityTask && isAwaitingReply ? ( <> {runtimeAdvisoryTone === 'error' ? ( ) : ( )} {runtimeAdvisoryLabel ?? 'awaiting reply'} ) : null}
{showStartingSkeleton ? (