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

159 lines
5.6 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import {
formatTeamModelSummary,
getTeamEffortLabel,
getTeamModelLabel,
getTeamProviderLabel,
} from '@renderer/components/team/dialogs/TeamModelSelector';
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { MemberCard } from './MemberCard';
import type { TaskStatusCounts } from '@renderer/utils/pathNormalize';
import type {
LeadActivityState,
MemberSpawnStatusEntry,
ResolvedTeamMember,
TeamTaskWithKanban,
} from '@shared/types';
interface MemberListProps {
members: ResolvedTeamMember[];
memberTaskCounts?: Map<string, TaskStatusCounts>;
taskMap?: Map<string, TeamTaskWithKanban>;
pendingRepliesByMember?: Record<string, number>;
memberSpawnStatuses?: Map<string, MemberSpawnStatusEntry>;
isTeamAlive?: boolean;
isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState;
launchParams?: TeamLaunchParams;
onMemberClick?: (member: ResolvedTeamMember) => void;
onSendMessage?: (member: ResolvedTeamMember) => void;
onAssignTask?: (member: ResolvedTeamMember) => void;
onOpenTask?: (task: TeamTaskWithKanban) => void;
}
export const MemberList = ({
members,
memberTaskCounts,
taskMap,
pendingRepliesByMember,
memberSpawnStatuses,
isTeamAlive,
isTeamProvisioning,
leadActivity,
launchParams,
onMemberClick,
onSendMessage,
onAssignTask,
onOpenTask,
}: 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 = members
.filter((m) => !m.removedAt)
.sort((a, b) => {
if (isLeadMember(a)) return -1;
if (isLeadMember(b)) return 1;
return 0;
});
const removedMembers = members.filter((m) => m.removedAt);
const colorMap = buildMemberColorMap(members);
const buildRuntimeSummary = useCallback(
(member: ResolvedTeamMember): string | undefined => {
const effectiveProvider = member.providerId ?? launchParams?.providerId ?? 'anthropic';
const effectiveModel = member.model?.trim() || launchParams?.model?.trim() || '';
const effectiveEffort = member.effort ?? launchParams?.effort;
return formatTeamModelSummary(effectiveProvider, effectiveModel, effectiveEffort);
},
[launchParams]
);
if (members.length === 0) {
return (
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
Solo team lead only
</div>
);
}
const renderCard = (member: ResolvedTeamMember, isRemoved: boolean): React.JSX.Element => {
const currentTask =
member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null;
const reviewTask = taskMap
? (Array.from(taskMap.values()).find(
(task) =>
task.reviewer === member.name &&
task.id !== member.currentTaskId &&
(task.reviewState === 'review' || task.kanbanColumn === 'review')
) ?? null)
: null;
const awaitingReply = Boolean(pendingRepliesByMember?.[member.name]);
const spawnEntry = memberSpawnStatuses?.get(member.name);
return (
<MemberCard
key={member.name}
member={member}
memberColor={colorMap.get(member.name) ?? 'blue'}
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
isTeamAlive={isTeamAlive}
isTeamProvisioning={isTeamProvisioning}
leadActivity={isLeadMember(member) ? leadActivity : undefined}
currentTask={isRemoved ? null : currentTask}
reviewTask={isRemoved ? null : reviewTask}
isAwaitingReply={isRemoved ? false : awaitingReply}
isRemoved={isRemoved}
runtimeSummary={isRemoved ? buildRuntimeSummary(member) : buildRuntimeSummary(member)}
spawnStatus={isRemoved ? undefined : spawnEntry?.status}
spawnError={isRemoved ? undefined : spawnEntry?.error}
spawnLivenessSource={isRemoved ? undefined : spawnEntry?.livenessSource}
spawnLaunchState={isRemoved ? undefined : spawnEntry?.launchState}
spawnRuntimeAlive={isRemoved ? undefined : spawnEntry?.runtimeAlive}
onOpenTask={!isRemoved && currentTask ? () => onOpenTask?.(currentTask) : undefined}
onOpenReviewTask={!isRemoved && reviewTask ? () => onOpenTask?.(reviewTask) : undefined}
onClick={() => onMemberClick?.(member)}
onSendMessage={() => onSendMessage?.(member)}
onAssignTask={() => onAssignTask?.(member)}
/>
);
};
return (
<div ref={containerRef} className="flex flex-col gap-1">
<div className={gridClass}>{activeMembers.map((member) => renderCard(member, false))}</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) => renderCard(member, true))}
</div>
</>
)}
</div>
);
};