From 193def1d599c8711d503b49f038a5d2bbebd289b Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 2 May 2026 21:43:24 +0500 Subject: [PATCH] perf: stabilize MemberCard callbacks via MemberCardRow wrapper Extract per-member rendering into a memo'd MemberCardRow component with useCallback handlers, so MemberCard never re-renders from new inline lambda references when MemberList re-renders for an unrelated member. --- .../components/team/members/MemberList.tsx | 236 ++++++++++++++---- 1 file changed, 187 insertions(+), 49 deletions(-) diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index 8b3f6140..115a298b 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -10,6 +10,9 @@ import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice'; import type { TaskStatusCounts } from '@renderer/utils/pathNormalize'; import type { LeadActivityState, + MemberLaunchState, + MemberSpawnLivenessSource, + MemberSpawnStatus, MemberSpawnStatusEntry, ResolvedTeamMember, TeamAgentRuntimeEntry, @@ -255,6 +258,115 @@ function areMemberListPropsEqual( ); } +// --------------------------------------------------------------------------- +// 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; + 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; + onSkipMemberForLaunch?: (memberName: string) => Promise | void; +} + +const MemberCardRow = memo(function MemberCardRow({ + member, + isRemoved, + memberColor, + currentTask, + reviewTask, + 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 ( + + ); +}); + export const MemberList = memo(function MemberList({ members, memberTaskCounts, @@ -340,63 +452,89 @@ export const MemberList = memo(function MemberList({ return result; }, [taskMap]); - const renderCard = (member: ResolvedTeamMember, isRemoved: boolean): React.JSX.Element => { - const currentTask = - member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null; - const reviewCandidate = reviewTaskByMember.get(member.name) ?? null; - const reviewTask = - reviewCandidate && reviewCandidate.id !== member.currentTaskId ? reviewCandidate : null; - const awaitingReply = isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name]); - const spawnEntry = memberSpawnStatuses?.get(member.name); - const runtimeEntry = memberRuntimeEntries?.get(member.name); - return ( - onOpenTask?.(currentTask.id) : undefined} - onOpenReviewTask={!isRemoved && reviewTask ? () => onOpenTask?.(reviewTask.id) : undefined} - onClick={() => onMemberClick?.(member)} - onSendMessage={() => onSendMessage?.(member)} - onAssignTask={() => onAssignTask?.(member)} - onRestartMember={isRemoved ? undefined : onRestartMember} - onSkipMemberForLaunch={isRemoved ? undefined : onSkipMemberForLaunch} - /> - ); - }; - return (
-
{activeMembers.map((member) => renderCard(member, false))}
+
+ {activeMembers.map((member) => { + const currentTask = + member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null; + const reviewCandidate = reviewTaskByMember.get(member.name) ?? null; + const reviewTask = + reviewCandidate && reviewCandidate.id !== member.currentTaskId ? reviewCandidate : null; + const spawnEntry = memberSpawnStatuses?.get(member.name); + const runtimeEntry = memberRuntimeEntries?.get(member.name); + return ( + + ); + })} +
{removedMembers.length > 0 && ( <>
Removed ({removedMembers.length})
- {removedMembers.map((member) => renderCard(member, true))} + {removedMembers.map((member) => ( + + ))}
)}