agent-ecosystem/src/renderer/components/team/members/MemberList.tsx

801 lines
28 KiB
TypeScript

import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
deriveReviewActivityTimerAnchor,
deriveWorkActivityTimerAnchor,
syncMemberActivityTimer,
} from '@renderer/utils/memberActivityTimer';
import { buildMemberColorMap, shouldDisplayMemberCurrentTask } from '@renderer/utils/memberHelpers';
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { isLeadMember } from '@shared/utils/leadDetection';
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
import { MemberCard } from './MemberCard';
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
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 MemberListProps {
teamName?: string;
members: ResolvedTeamMember[];
expectedTeammateCount?: number;
memberTaskCounts?: Map<string, TaskStatusCounts>;
taskMap?: Map<string, TeamTaskWithKanban>;
pendingRepliesByMember?: Record<string, number>;
memberSpawnStatuses?: Map<string, MemberSpawnStatusEntry>;
memberRuntimeEntries?: Map<string, TeamAgentRuntimeEntry>;
runtimeRunId?: string | null;
isLaunchSettling?: boolean;
isTeamAlive?: boolean;
isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState;
launchParams?: TeamLaunchParams;
onMemberClick?: (member: ResolvedTeamMember) => void;
onSendMessage?: (member: ResolvedTeamMember) => void;
onAssignTask?: (member: ResolvedTeamMember) => void;
onOpenTask?: (taskId: string) => void;
onRestartMember?: (memberName: string) => Promise<void> | void;
onSkipMemberForLaunch?: (memberName: string) => Promise<void> | void;
}
function areResolvedMembersEquivalent(
left: readonly ResolvedTeamMember[],
right: readonly ResolvedTeamMember[]
): boolean {
if (left === right) return true;
if (left.length !== right.length) return false;
for (let index = 0; index < left.length; index += 1) {
const leftMember = left[index];
const rightMember = right[index];
if (
leftMember.name !== rightMember.name ||
leftMember.status !== rightMember.status ||
leftMember.currentTaskId !== rightMember.currentTaskId ||
leftMember.taskCount !== rightMember.taskCount ||
leftMember.color !== rightMember.color ||
leftMember.agentType !== rightMember.agentType ||
leftMember.role !== rightMember.role ||
leftMember.workflow !== rightMember.workflow ||
leftMember.providerId !== rightMember.providerId ||
leftMember.model !== rightMember.model ||
leftMember.effort !== rightMember.effort ||
leftMember.cwd !== rightMember.cwd ||
leftMember.gitBranch !== rightMember.gitBranch ||
leftMember.removedAt !== rightMember.removedAt ||
leftMember.runtimeAdvisory?.kind !== rightMember.runtimeAdvisory?.kind ||
leftMember.runtimeAdvisory?.observedAt !== rightMember.runtimeAdvisory?.observedAt ||
leftMember.runtimeAdvisory?.retryUntil !== rightMember.runtimeAdvisory?.retryUntil ||
leftMember.runtimeAdvisory?.retryDelayMs !== rightMember.runtimeAdvisory?.retryDelayMs ||
leftMember.runtimeAdvisory?.reasonCode !== rightMember.runtimeAdvisory?.reasonCode ||
leftMember.runtimeAdvisory?.message !== rightMember.runtimeAdvisory?.message
) {
return false;
}
}
return true;
}
function areTaskStatusCountsMapsEquivalent(
left: Map<string, TaskStatusCounts> | undefined,
right: Map<string, TaskStatusCounts> | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.size !== right.size) return false;
for (const [key, leftCounts] of left) {
const rightCounts = right.get(key);
if (
leftCounts.pending !== rightCounts?.pending ||
leftCounts.inProgress !== rightCounts.inProgress ||
leftCounts.completed !== rightCounts.completed
) {
return false;
}
}
return true;
}
function areTaskWorkIntervalsEquivalent(
left: TeamTaskWithKanban['workIntervals'],
right: TeamTaskWithKanban['workIntervals']
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.length !== right.length) return false;
return left.every((interval, index) => {
const other = right[index];
if (!other) return false;
return interval.startedAt === other.startedAt && interval.completedAt === other.completedAt;
});
}
function areTaskReviewIntervalsEquivalent(
left: TeamTaskWithKanban['reviewIntervals'],
right: TeamTaskWithKanban['reviewIntervals']
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.length !== right.length) return false;
return left.every((interval, index) => {
const other = right[index];
if (!other) return false;
return (
interval.reviewer === other.reviewer &&
interval.startedAt === other.startedAt &&
interval.completedAt === other.completedAt
);
});
}
function areTaskHistoryEventsEquivalent(
left: TeamTaskWithKanban['historyEvents'],
right: TeamTaskWithKanban['historyEvents']
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.length !== right.length) return false;
return left.every((event, index) => {
const other = right[index];
if (!other) return false;
const leftRow = event as unknown as Record<string, unknown>;
const rightRow = other as unknown as Record<string, unknown>;
return (
event.id === other.id &&
event.type === other.type &&
event.timestamp === other.timestamp &&
leftRow.actor === rightRow.actor &&
leftRow.reviewer === rightRow.reviewer &&
leftRow.from === rightRow.from &&
leftRow.to === rightRow.to &&
leftRow.status === rightRow.status
);
});
}
function areMemberTaskMapsEquivalent(
left: Map<string, TeamTaskWithKanban> | undefined,
right: Map<string, TeamTaskWithKanban> | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.size !== right.size) return false;
for (const [key, leftTask] of left) {
const rightTask = right.get(key);
if (
leftTask.id !== rightTask?.id ||
leftTask.displayId !== rightTask.displayId ||
leftTask.subject !== rightTask.subject ||
leftTask.owner !== rightTask.owner ||
leftTask.status !== rightTask.status ||
leftTask.reviewer !== rightTask.reviewer ||
leftTask.reviewState !== rightTask.reviewState ||
leftTask.kanbanColumn !== rightTask.kanbanColumn ||
!areTaskWorkIntervalsEquivalent(leftTask.workIntervals, rightTask.workIntervals) ||
!areTaskReviewIntervalsEquivalent(leftTask.reviewIntervals, rightTask.reviewIntervals) ||
!areTaskHistoryEventsEquivalent(leftTask.historyEvents, rightTask.historyEvents)
) {
return false;
}
}
return true;
}
function arePendingRepliesEquivalent(
left: Record<string, number> | undefined,
right: Record<string, number> | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if (leftKeys.length !== rightKeys.length) return false;
for (const key of leftKeys) {
if (left[key] !== right[key]) {
return false;
}
}
return true;
}
function areMemberSpawnStatusesEquivalent(
left: Map<string, MemberSpawnStatusEntry> | undefined,
right: Map<string, MemberSpawnStatusEntry> | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.size !== right.size) return false;
for (const [key, leftEntry] of left) {
const rightEntry = right.get(key);
if (
leftEntry.status !== rightEntry?.status ||
leftEntry.launchState !== rightEntry.launchState ||
leftEntry.error !== rightEntry.error ||
leftEntry.hardFailure !== rightEntry.hardFailure ||
leftEntry.hardFailureReason !== rightEntry.hardFailureReason ||
leftEntry.skippedForLaunch !== rightEntry.skippedForLaunch ||
leftEntry.skipReason !== rightEntry.skipReason ||
leftEntry.skippedAt !== rightEntry.skippedAt ||
leftEntry.livenessSource !== rightEntry.livenessSource ||
leftEntry.livenessKind !== rightEntry.livenessKind ||
leftEntry.runtimeDiagnostic !== rightEntry.runtimeDiagnostic ||
leftEntry.runtimeDiagnosticSeverity !== rightEntry.runtimeDiagnosticSeverity ||
leftEntry.runtimeModel !== rightEntry.runtimeModel ||
leftEntry.runtimeAlive !== rightEntry.runtimeAlive ||
leftEntry.bootstrapConfirmed !== rightEntry.bootstrapConfirmed ||
leftEntry.agentToolAccepted !== rightEntry.agentToolAccepted ||
(leftEntry.pendingPermissionRequestIds ?? []).join('\0') !==
(rightEntry.pendingPermissionRequestIds ?? []).join('\0')
) {
return false;
}
}
return true;
}
function areLaunchParamsEquivalent(
left: TeamLaunchParams | undefined,
right: TeamLaunchParams | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
return (
left.providerId === right.providerId &&
left.providerBackendId === right.providerBackendId &&
left.model === right.model &&
left.effort === right.effort &&
left.fastMode === right.fastMode &&
left.limitContext === right.limitContext
);
}
function areMemberRuntimeEntriesEquivalent(
left: Map<string, TeamAgentRuntimeEntry> | undefined,
right: Map<string, TeamAgentRuntimeEntry> | undefined
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
if (left.size !== right.size) return false;
for (const [key, leftEntry] of left) {
const rightEntry = right.get(key);
const leftDiagnostics = leftEntry.diagnostics ?? [];
const rightDiagnostics = rightEntry?.diagnostics ?? [];
if (
leftEntry.memberName !== rightEntry?.memberName ||
leftEntry.alive !== rightEntry?.alive ||
leftEntry.restartable !== rightEntry?.restartable ||
leftEntry.backendType !== rightEntry?.backendType ||
leftEntry.providerId !== rightEntry?.providerId ||
leftEntry.providerBackendId !== rightEntry?.providerBackendId ||
leftEntry.laneId !== rightEntry?.laneId ||
leftEntry.laneKind !== rightEntry?.laneKind ||
leftEntry.pid !== rightEntry?.pid ||
leftEntry.runtimeModel !== rightEntry?.runtimeModel ||
leftEntry.rssBytes !== rightEntry?.rssBytes ||
leftEntry.livenessKind !== rightEntry?.livenessKind ||
leftEntry.pidSource !== rightEntry?.pidSource ||
leftEntry.processCommand !== rightEntry?.processCommand ||
leftEntry.paneId !== rightEntry?.paneId ||
leftEntry.panePid !== rightEntry?.panePid ||
leftEntry.paneCurrentCommand !== rightEntry?.paneCurrentCommand ||
leftEntry.runtimePid !== rightEntry?.runtimePid ||
leftEntry.runtimeSessionId !== rightEntry?.runtimeSessionId ||
leftEntry.runtimeDiagnostic !== rightEntry?.runtimeDiagnostic ||
leftEntry.runtimeDiagnosticSeverity !== rightEntry?.runtimeDiagnosticSeverity ||
leftEntry.runtimeLastSeenAt !== rightEntry?.runtimeLastSeenAt ||
leftEntry.historicalBootstrapConfirmed !== rightEntry?.historicalBootstrapConfirmed ||
leftDiagnostics.length !== rightDiagnostics.length ||
!leftDiagnostics.every((value, index) => value === rightDiagnostics[index])
) {
return false;
}
}
return true;
}
function areMemberListPropsEqual(
prev: Readonly<MemberListProps>,
next: Readonly<MemberListProps>
): boolean {
return (
prev.teamName === next.teamName &&
areResolvedMembersEquivalent(prev.members, next.members) &&
prev.expectedTeammateCount === next.expectedTeammateCount &&
areTaskStatusCountsMapsEquivalent(prev.memberTaskCounts, next.memberTaskCounts) &&
areMemberTaskMapsEquivalent(prev.taskMap, next.taskMap) &&
arePendingRepliesEquivalent(prev.pendingRepliesByMember, next.pendingRepliesByMember) &&
areMemberSpawnStatusesEquivalent(prev.memberSpawnStatuses, next.memberSpawnStatuses) &&
areMemberRuntimeEntriesEquivalent(prev.memberRuntimeEntries, next.memberRuntimeEntries) &&
prev.runtimeRunId === next.runtimeRunId &&
prev.isLaunchSettling === next.isLaunchSettling &&
prev.isTeamAlive === next.isTeamAlive &&
prev.isTeamProvisioning === next.isTeamProvisioning &&
prev.leadActivity === next.leadActivity &&
prev.onRestartMember === next.onRestartMember &&
prev.onSkipMemberForLaunch === next.onSkipMemberForLaunch &&
areLaunchParamsEquivalent(prev.launchParams, next.launchParams)
);
}
// ---------------------------------------------------------------------------
// Per-member row wrapper — creates stable callbacks so MemberCard memo holds
// ---------------------------------------------------------------------------
interface MemberCardRowProps {
member: ResolvedTeamMember;
isRemoved: boolean;
memberColor: string;
currentTask: TeamTaskWithKanban | null;
reviewTask: TeamTaskWithKanban | null;
currentTaskTimer: MemberActivityTimerAnchor | null;
reviewTaskTimer: MemberActivityTimerAnchor | null;
currentTaskTimerRunning: boolean;
reviewTaskTimerRunning: boolean;
awaitingReply: boolean;
taskCounts?: TaskStatusCounts | null;
runtimeSummary?: string;
runtimeEntry?: TeamAgentRuntimeEntry;
runtimeRunId?: string | null;
spawnStatus?: MemberSpawnStatus;
spawnEntry?: MemberSpawnStatusEntry;
spawnError?: string;
spawnLivenessSource?: MemberSpawnLivenessSource;
spawnLaunchState?: MemberLaunchState;
spawnRuntimeAlive?: boolean;
isTeamAlive?: boolean;
isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState;
isLaunchSettling?: boolean;
onOpenTask?: (taskId: string) => void;
onMemberClick?: (member: ResolvedTeamMember) => void;
onSendMessage?: (member: ResolvedTeamMember) => void;
onAssignTask?: (member: ResolvedTeamMember) => void;
onRestartMember?: (memberName: string) => Promise<void> | void;
onSkipMemberForLaunch?: (memberName: string) => Promise<void> | void;
}
const MemberCardRow = memo(function MemberCardRow({
member,
isRemoved,
memberColor,
currentTask,
reviewTask,
currentTaskTimer,
reviewTaskTimer,
currentTaskTimerRunning,
reviewTaskTimerRunning,
awaitingReply,
taskCounts,
runtimeSummary,
runtimeEntry,
runtimeRunId,
spawnStatus,
spawnEntry,
spawnError,
spawnLivenessSource,
spawnLaunchState,
spawnRuntimeAlive,
isTeamAlive,
isTeamProvisioning,
leadActivity,
isLaunchSettling,
onOpenTask,
onMemberClick,
onSendMessage,
onAssignTask,
onRestartMember,
onSkipMemberForLaunch,
}: MemberCardRowProps): React.JSX.Element {
const currentTaskId = currentTask?.id;
const reviewTaskId = reviewTask?.id;
const handleOpenTask = useCallback(() => {
if (currentTaskId) onOpenTask?.(currentTaskId);
}, [onOpenTask, currentTaskId]);
const handleOpenReviewTask = useCallback(() => {
if (reviewTaskId) onOpenTask?.(reviewTaskId);
}, [onOpenTask, reviewTaskId]);
const handleClick = useCallback(() => onMemberClick?.(member), [onMemberClick, member]);
const handleSendMessage = useCallback(() => onSendMessage?.(member), [onSendMessage, member]);
const handleAssignTask = useCallback(() => onAssignTask?.(member), [onAssignTask, member]);
return (
<MemberCard
member={member}
memberColor={memberColor}
taskCounts={taskCounts}
isTeamAlive={isTeamAlive}
isTeamProvisioning={isTeamProvisioning}
leadActivity={isLeadMember(member) ? leadActivity : undefined}
currentTask={currentTask}
reviewTask={reviewTask}
currentTaskTimer={currentTaskTimer}
reviewTaskTimer={reviewTaskTimer}
currentTaskTimerRunning={currentTaskTimerRunning}
reviewTaskTimerRunning={reviewTaskTimerRunning}
isAwaitingReply={awaitingReply}
isRemoved={isRemoved}
runtimeSummary={runtimeSummary}
runtimeEntry={runtimeEntry}
runtimeRunId={runtimeRunId}
spawnStatus={spawnStatus}
spawnEntry={spawnEntry}
spawnError={spawnError}
spawnLivenessSource={spawnLivenessSource}
spawnLaunchState={spawnLaunchState}
spawnRuntimeAlive={spawnRuntimeAlive}
isLaunchSettling={isLaunchSettling}
onOpenTask={currentTask ? handleOpenTask : undefined}
onOpenReviewTask={reviewTask ? handleOpenReviewTask : undefined}
onClick={handleClick}
onSendMessage={handleSendMessage}
onAssignTask={handleAssignTask}
onRestartMember={onRestartMember}
onSkipMemberForLaunch={onSkipMemberForLaunch}
/>
);
});
export const MemberList = memo(function MemberList({
teamName = '__unknown_team__',
members,
expectedTeammateCount,
memberTaskCounts,
taskMap,
pendingRepliesByMember,
memberSpawnStatuses,
memberRuntimeEntries,
runtimeRunId,
isLaunchSettling,
isTeamAlive,
isTeamProvisioning,
leadActivity,
launchParams,
onMemberClick,
onSendMessage,
onAssignTask,
onOpenTask,
onRestartMember,
onSkipMemberForLaunch,
}: MemberListProps): React.JSX.Element {
const containerRef = useRef<HTMLDivElement>(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 = useMemo(
() =>
members
.filter((m) => !m.removedAt)
.sort((a, b) => {
if (isLeadMember(a)) return -1;
if (isLeadMember(b)) return 1;
return 0;
}),
[members]
);
const removedMembers = useMemo(() => members.filter((m) => m.removedAt), [members]);
const activeTeammateCount = useMemo(
() => activeMembers.filter((member) => !isLeadMember(member)).length,
[activeMembers]
);
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
// Pre-compute reviewer->task map to avoid O(n*n) scan per member.
const reviewTaskByMember = useMemo(() => {
const result = new Map<string, TeamTaskWithKanban>();
if (!taskMap) return result;
for (const task of taskMap.values()) {
if (task.reviewer && getTeamTaskWorkflowColumn(task) === 'review') {
result.set(task.reviewer, task);
}
}
return result;
}, [taskMap]);
const isMemberActivityTimerRunning = useCallback(
(
spawnEntry: MemberSpawnStatusEntry | undefined,
runtimeEntry: TeamAgentRuntimeEntry | undefined
): boolean => {
if (isTeamAlive === false) return false;
if (
spawnEntry?.status === 'offline' ||
spawnEntry?.status === 'error' ||
spawnEntry?.status === 'skipped'
) {
return false;
}
if (spawnEntry?.runtimeAlive === false && spawnEntry.status !== 'online') {
return false;
}
if (
runtimeEntry?.livenessKind === 'shell_only' ||
runtimeEntry?.livenessKind === 'registered_only' ||
runtimeEntry?.livenessKind === 'stale_metadata' ||
runtimeEntry?.livenessKind === 'not_found'
) {
return false;
}
return true;
},
[isTeamAlive]
);
const getActivityTimerRunId = useCallback(
(running: boolean): string | null => {
if (!running) return null;
return runtimeRunId ?? 'runtime:unknown';
},
[runtimeRunId]
);
const withActivityTimerRunId = useCallback(
(
anchor: MemberActivityTimerAnchor | null,
running: boolean
): MemberActivityTimerAnchor | null => {
if (!anchor) return null;
return {
...anchor,
runId: getActivityTimerRunId(running),
};
},
[getActivityTimerRunId]
);
useEffect(() => {
if (!taskMap) return;
const nowMs = Date.now();
for (const member of activeMembers) {
const spawnEntry = memberSpawnStatuses?.get(member.name);
const runtimeEntry = memberRuntimeEntries?.get(member.name);
const running = isMemberActivityTimerRunning(spawnEntry, runtimeEntry);
const currentTaskCandidate = member.currentTaskId
? (taskMap.get(member.currentTaskId) ?? null)
: null;
if (isDisplayableCurrentTask(currentTaskCandidate)) {
const anchor = deriveWorkActivityTimerAnchor(currentTaskCandidate, {
teamName,
memberName: member.name,
});
if (anchor) {
const visible =
running &&
shouldDisplayMemberCurrentTask({
member,
isTeamAlive,
spawnStatus: spawnEntry?.status,
spawnLaunchState: spawnEntry?.launchState,
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
runtimeEntry,
});
syncMemberActivityTimer({
timerId: anchor.timerId,
startedAtMs: anchor.startedAtMs,
baseElapsedMs: anchor.baseElapsedMs,
running: visible,
runId: getActivityTimerRunId(visible),
nowMs,
});
}
}
const reviewTask = reviewTaskByMember.get(member.name) ?? null;
if (reviewTask) {
const anchor = deriveReviewActivityTimerAnchor(reviewTask, {
teamName,
memberName: member.name,
});
if (anchor) {
syncMemberActivityTimer({
timerId: anchor.timerId,
startedAtMs: anchor.startedAtMs,
baseElapsedMs: anchor.baseElapsedMs,
running,
runId: getActivityTimerRunId(running),
nowMs,
});
}
}
}
}, [
activeMembers,
getActivityTimerRunId,
isMemberActivityTimerRunning,
isTeamAlive,
memberRuntimeEntries,
memberSpawnStatuses,
reviewTaskByMember,
taskMap,
teamName,
]);
const buildRuntimeSummary = useCallback(
(
member: ResolvedTeamMember,
spawnEntry: MemberSpawnStatusEntry | undefined,
runtimeEntry: TeamAgentRuntimeEntry | undefined
): string | undefined => {
return resolveMemberRuntimeSummary(member, launchParams, spawnEntry, runtimeEntry);
},
[launchParams]
);
const expectsTeammates = (expectedTeammateCount ?? 0) > 0;
const hasOnlyLeadWhileTeammatesLoad =
expectsTeammates && activeTeammateCount === 0 && removedMembers.length === 0;
if (members.length === 0 || hasOnlyLeadWhileTeammatesLoad) {
if (expectsTeammates) {
return (
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
Team members are loading
</div>
);
}
return (
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
Solo team - lead only
</div>
);
}
return (
<div ref={containerRef} className="flex flex-col gap-1">
<div className={gridClass}>
{activeMembers.map((member) => {
const spawnEntry = memberSpawnStatuses?.get(member.name);
const runtimeEntry = memberRuntimeEntries?.get(member.name);
const currentTaskCandidate =
member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null;
const currentTask =
isDisplayableCurrentTask(currentTaskCandidate) &&
shouldDisplayMemberCurrentTask({
member,
isTeamAlive,
spawnStatus: spawnEntry?.status,
spawnLaunchState: spawnEntry?.launchState,
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
runtimeEntry,
})
? currentTaskCandidate
: null;
const reviewCandidate = reviewTaskByMember.get(member.name) ?? null;
const reviewTask =
reviewCandidate && reviewCandidate.id !== currentTask?.id ? reviewCandidate : null;
const activityTimerRunning = isMemberActivityTimerRunning(spawnEntry, runtimeEntry);
const currentTaskTimer = withActivityTimerRunId(
currentTask
? deriveWorkActivityTimerAnchor(currentTask, {
teamName,
memberName: member.name,
})
: null,
activityTimerRunning
);
const reviewTaskTimer = withActivityTimerRunId(
reviewTask
? deriveReviewActivityTimerAnchor(reviewTask, {
teamName,
memberName: member.name,
})
: null,
activityTimerRunning
);
return (
<MemberCardRow
key={member.name}
member={member}
isRemoved={false}
memberColor={colorMap.get(member.name) ?? 'blue'}
currentTask={currentTask}
reviewTask={reviewTask}
currentTaskTimer={currentTaskTimer}
reviewTaskTimer={reviewTaskTimer}
currentTaskTimerRunning={activityTimerRunning}
reviewTaskTimerRunning={activityTimerRunning}
awaitingReply={
isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name])
}
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)}
runtimeEntry={runtimeEntry}
runtimeRunId={runtimeRunId}
spawnStatus={spawnEntry?.status}
spawnEntry={spawnEntry}
spawnError={spawnEntry?.error ?? spawnEntry?.hardFailureReason}
spawnLivenessSource={spawnEntry?.livenessSource}
spawnLaunchState={spawnEntry?.launchState}
spawnRuntimeAlive={spawnEntry?.runtimeAlive}
isTeamAlive={isTeamAlive}
isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity}
isLaunchSettling={isLaunchSettling}
onOpenTask={onOpenTask}
onMemberClick={onMemberClick}
onSendMessage={onSendMessage}
onAssignTask={onAssignTask}
onRestartMember={onRestartMember}
onSkipMemberForLaunch={onSkipMemberForLaunch}
/>
);
})}
</div>
{removedMembers.length > 0 && (
<>
<div className="mt-2 text-[10px] text-[var(--color-text-muted)]">
Removed ({removedMembers.length})
</div>
<div className={gridClass}>
{removedMembers.map((member) => (
<MemberCardRow
key={member.name}
member={member}
isRemoved={true}
memberColor={colorMap.get(member.name) ?? 'blue'}
currentTask={null}
reviewTask={null}
currentTaskTimer={null}
reviewTaskTimer={null}
currentTaskTimerRunning={false}
reviewTaskTimerRunning={false}
awaitingReply={false}
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
runtimeSummary={buildRuntimeSummary(member, undefined, undefined)}
runtimeEntry={undefined}
runtimeRunId={undefined}
spawnStatus={undefined}
spawnEntry={undefined}
spawnError={undefined}
spawnLivenessSource={undefined}
spawnLaunchState={undefined}
spawnRuntimeAlive={undefined}
isTeamAlive={isTeamAlive}
isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity}
isLaunchSettling={false}
onOpenTask={onOpenTask}
onMemberClick={onMemberClick}
onSendMessage={onSendMessage}
onAssignTask={onAssignTask}
onRestartMember={undefined}
onSkipMemberForLaunch={undefined}
/>
))}
</div>
</>
)}
</div>
);
}, areMemberListPropsEqual);