diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index 69b58351..179d4e1d 100644 --- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -11,8 +11,10 @@ import { getUnreadCount } from '@renderer/services/commentReadStorage'; import { agentAvatarUrl, + buildMemberAvatarMap, buildMemberLaunchPresentation, getMemberRuntimeAdvisoryLabel, + resolveMemberAvatarUrl, } from '@renderer/utils/memberHelpers'; import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation'; import { formatTeamRuntimeSummary } from '@renderer/utils/teamRuntimeSummary'; @@ -143,6 +145,7 @@ export class TeamGraphAdapter { const leadId = `lead:${teamName}`; const leadName = TeamGraphAdapter.#getLeadMemberName(teamData, teamName); const memberNodeIdByAlias = TeamGraphAdapter.#buildMemberNodeIdByAlias(teamData, teamName); + const avatarMap = buildMemberAvatarMap(teamData.members); const provisioningPresentation = buildTeamProvisioningPresentation({ progress: provisioningProgress, members: teamData.members, @@ -158,6 +161,7 @@ export class TeamGraphAdapter { teamData, teamName, leadName, + avatarMap, pendingApprovalAgents, leadActivity, leadContext, @@ -173,6 +177,7 @@ export class TeamGraphAdapter { teamData, teamName, memberNodeIdByAlias, + avatarMap, spawnStatuses, pendingApprovalAgents, activeTools, @@ -369,6 +374,7 @@ export class TeamGraphAdapter { data: TeamGraphData, teamName: string, leadName: string, + avatarMap: ReadonlyMap, pendingApprovalAgents?: Set, leadActivity?: LeadActivityState, leadContext?: LeadContextUsage, @@ -428,7 +434,9 @@ export class TeamGraphAdapter { launchVisualState: leadLaunchPresentation?.launchVisualState ?? undefined, launchStatusLabel: leadLaunchPresentation?.launchStatusLabel ?? undefined, contextUsage: percent != null ? Math.max(0, Math.min(1, percent / 100)) : undefined, - avatarUrl: agentAvatarUrl(leadName, 64), + avatarUrl: leadMember + ? resolveMemberAvatarUrl(leadMember, avatarMap, 64) + : agentAvatarUrl(leadName, 64), pendingApproval, activeTool: activeTool ? { @@ -465,6 +473,7 @@ export class TeamGraphAdapter { data: TeamGraphData, teamName: string, memberNodeIdByAlias: ReadonlyMap, + avatarMap: ReadonlyMap, spawnStatuses?: Record, pendingApprovalAgents?: Set, activeTools?: Record>, @@ -520,7 +529,7 @@ export class TeamGraphAdapter { spawnStatus: spawn?.status, launchVisualState: launchPresentation.launchVisualState ?? undefined, launchStatusLabel: launchPresentation.launchStatusLabel ?? undefined, - avatarUrl: agentAvatarUrl(member.name, 64), + avatarUrl: resolveMemberAvatarUrl(member, avatarMap, 64), currentTaskId: member.currentTaskId ?? undefined, currentTaskSubject: member.currentTaskId ? data.tasks.find((t) => t.id === member.currentTaskId)?.subject diff --git a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx index a25e3c84..f6794aa1 100644 --- a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx @@ -4,9 +4,15 @@ * composes project-specific UI, selectors, and presentation helpers. */ +import { useMemo } from 'react'; + import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; -import { agentAvatarUrl, buildMemberLaunchPresentation } from '@renderer/utils/memberHelpers'; +import { + agentAvatarUrl, + buildMemberAvatarMap, + buildMemberLaunchPresentation, +} from '@renderer/utils/memberHelpers'; import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation'; import { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react'; @@ -291,7 +297,6 @@ const MemberPopoverContent = ({ node.domainRef.kind === 'member' || node.domainRef.kind === 'lead' ? node.domainRef.teamName : ''; - const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64); const { teamData, teamMembers, @@ -301,6 +306,8 @@ const MemberPopoverContent = ({ memberSpawnSnapshot, memberSpawnStatuses, } = useGraphMemberPopoverContext(teamName, memberName); + const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); + const avatarSrc = node.avatarUrl ?? avatarMap.get(memberName) ?? agentAvatarUrl(memberName, 64); const member = teamMembers.find((candidate) => candidate.name === memberName) ?? null; const provisioningPresentation = teamData && teamName diff --git a/src/renderer/assets/participant-avatars/01.png b/src/renderer/assets/participant-avatars/01.png new file mode 100644 index 00000000..4128d3b0 Binary files /dev/null and b/src/renderer/assets/participant-avatars/01.png differ diff --git a/src/renderer/assets/participant-avatars/02.png b/src/renderer/assets/participant-avatars/02.png new file mode 100644 index 00000000..15575859 Binary files /dev/null and b/src/renderer/assets/participant-avatars/02.png differ diff --git a/src/renderer/assets/participant-avatars/03.png b/src/renderer/assets/participant-avatars/03.png new file mode 100644 index 00000000..a5e00bcd Binary files /dev/null and b/src/renderer/assets/participant-avatars/03.png differ diff --git a/src/renderer/assets/participant-avatars/04.png b/src/renderer/assets/participant-avatars/04.png new file mode 100644 index 00000000..f984db69 Binary files /dev/null and b/src/renderer/assets/participant-avatars/04.png differ diff --git a/src/renderer/assets/participant-avatars/05.png b/src/renderer/assets/participant-avatars/05.png new file mode 100644 index 00000000..a9795962 Binary files /dev/null and b/src/renderer/assets/participant-avatars/05.png differ diff --git a/src/renderer/assets/participant-avatars/06.png b/src/renderer/assets/participant-avatars/06.png new file mode 100644 index 00000000..71950d32 Binary files /dev/null and b/src/renderer/assets/participant-avatars/06.png differ diff --git a/src/renderer/assets/participant-avatars/07.png b/src/renderer/assets/participant-avatars/07.png new file mode 100644 index 00000000..8f23fb86 Binary files /dev/null and b/src/renderer/assets/participant-avatars/07.png differ diff --git a/src/renderer/assets/participant-avatars/08.png b/src/renderer/assets/participant-avatars/08.png new file mode 100644 index 00000000..c7ada81e Binary files /dev/null and b/src/renderer/assets/participant-avatars/08.png differ diff --git a/src/renderer/assets/participant-avatars/09.png b/src/renderer/assets/participant-avatars/09.png new file mode 100644 index 00000000..8f4abe98 Binary files /dev/null and b/src/renderer/assets/participant-avatars/09.png differ diff --git a/src/renderer/assets/participant-avatars/10.png b/src/renderer/assets/participant-avatars/10.png new file mode 100644 index 00000000..bee2490e Binary files /dev/null and b/src/renderer/assets/participant-avatars/10.png differ diff --git a/src/renderer/assets/participant-avatars/11.png b/src/renderer/assets/participant-avatars/11.png new file mode 100644 index 00000000..e77da7e4 Binary files /dev/null and b/src/renderer/assets/participant-avatars/11.png differ diff --git a/src/renderer/assets/participant-avatars/12.png b/src/renderer/assets/participant-avatars/12.png new file mode 100644 index 00000000..32ee4912 Binary files /dev/null and b/src/renderer/assets/participant-avatars/12.png differ diff --git a/src/renderer/assets/participant-avatars/13.png b/src/renderer/assets/participant-avatars/13.png new file mode 100644 index 00000000..9b774e24 Binary files /dev/null and b/src/renderer/assets/participant-avatars/13.png differ diff --git a/src/renderer/components/team/MemberBadge.tsx b/src/renderer/components/team/MemberBadge.tsx index 6ac4efda..7f29c0ff 100644 --- a/src/renderer/components/team/MemberBadge.tsx +++ b/src/renderer/components/team/MemberBadge.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; + import { getTeamColorSet, getThemedBadge, @@ -5,7 +7,13 @@ import { getThemedText, } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; -import { agentAvatarUrl, displayMemberName } from '@renderer/utils/memberHelpers'; +import { useStore } from '@renderer/store'; +import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; +import { + agentAvatarUrl, + buildMemberAvatarMap, + displayMemberName, +} from '@renderer/utils/memberHelpers'; import { MemberHoverCard } from './members/MemberHoverCard'; @@ -40,6 +48,12 @@ export const MemberBadge = ({ }: MemberBadgeProps): React.JSX.Element => { const colors = getTeamColorSet(color ?? ''); const { isLight } = useTheme(); + const selectedTeamName = useStore((s) => s.selectedTeamName); + const effectiveTeamName = teamName ?? selectedTeamName; + const teamMembers = useStore((s) => + effectiveTeamName ? selectResolvedMembersForTeamName(s, effectiveTeamName) : [] + ); + const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); const avatarSize = size === 'md' ? 32 : size === 'sm' ? 24 : 18; const avatarClass = size === 'md' ? 'size-6' : size === 'sm' ? 'size-5' : 'size-4'; const textClass = size === 'md' ? 'text-xs' : size === 'sm' ? 'text-[10px]' : 'text-[9px]'; @@ -53,7 +67,7 @@ export const MemberBadge = ({ const avatar = ( [t.id, t])); const entries: ActivityEntry[] = []; @@ -115,7 +117,7 @@ export const ActiveTasksBlock = ({
void; onReply?: (message: InboxMessage) => void; @@ -39,6 +40,7 @@ interface DialogThoughtsContentProps { const DialogThoughtsContent = ({ group, + members, memberColor, onTaskIdClick, onReply, @@ -51,6 +53,7 @@ const DialogThoughtsContent = ({ const newest = thoughts[0]; const oldest = thoughts[thoughts.length - 1]; const colors = getTeamColorSet(memberColor ?? ''); + const avatarMap = useMemo(() => buildMemberAvatarMap(members ?? []), [members]); const chronological = useMemo(() => [...thoughts].reverse(), [thoughts]); return ( @@ -58,7 +61,7 @@ const DialogThoughtsContent = ({ {/* Header */}
s.pendingApprovals)); const colorMap = buildMemberColorMap(members); + const avatarMap = buildMemberAvatarMap(members); const memberPending = Object.entries(pendingRepliesByMember) .map(([name, sentAtMs]) => ({ kind: 'member' as const, @@ -111,7 +113,7 @@ export const PendingRepliesBlock = ({
{ const colorMap = useMemo(() => buildMemberColorMap(members), [members]); + const avatarMap = useMemo(() => buildMemberAvatarMap(members), [members]); const { isLight } = useTheme(); const currentTask = task ? (taskMap.get(task.id) ?? task) : null; const updateTaskFields = useStore((s) => s.updateTaskFields); @@ -697,7 +699,10 @@ export const TaskDetailDialog = ({ style={reviewerBadgeStyle} > // member.agentType === 'team-lead' && teamName ? s.leadContextByTeam[teamName] : undefined // ); + const selectedTeamName = useStore((s) => s.selectedTeamName); + const teamMembers = useStore((s) => + selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] + ); + const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); const launchPresentation = buildMemberLaunchPresentation({ member, spawnStatus, @@ -173,7 +183,7 @@ export const MemberCard = ({ }} > {member.name} { const [editing, setEditing] = useState(false); + const selectedTeamName = useStore((s) => s.selectedTeamName); + const teamMembers = useStore((s) => + selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] + ); + const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); // NOTE: lead context display disabled — usage formula is inaccurate // const teamName = useStore((s) => s.selectedTeamName); @@ -84,7 +92,7 @@ export const MemberDetailHeader = ({
{member.name} s.openMemberProfile); + const avatarMap = buildMemberAvatarMap(teamMembers); if (!member) { return <>{children}; @@ -142,7 +144,7 @@ export const MemberHoverCard = ({
{member.name} buildMemberColorMap(members), [members]); + const avatarMap = React.useMemo(() => buildMemberAvatarMap(members), [members]); const selectedMember = React.useMemo( () => (value ? members.find((m) => m.name === value) : null), [members, value] @@ -60,7 +65,7 @@ export const MemberSelect = ({ return ( = { @@ -567,6 +591,12 @@ interface MemberColorInput { role?: string; } +interface MemberAvatarInput { + name: string; + removedAt?: number | string | null; + agentType?: string; +} + /** * Build a consistent name→colorName map for all members. * Active members receive colors sequentially from MEMBER_COLOR_PALETTE, @@ -578,6 +608,49 @@ export function buildMemberColorMap(members: MemberColorInput[]): Map { + const map = new Map(); + const activeMembers = members.filter((member) => !member.removedAt); + const leadMembers = activeMembers.filter((member) => isLeadMember(member)); + const teammateMembers = activeMembers.filter((member) => !isLeadMember(member)); + + for (const [index, member] of leadMembers.entries()) { + map.set(member.name, index === 0 ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name)); + } + + for (const [index, member] of teammateMembers.entries()) { + map.set( + member.name, + getParticipantAvatarUrlByIndex(1 + (index % (PARTICIPANT_AVATAR_URLS.length - 1))) + ); + } + + for (const member of members) { + if (!map.has(member.name)) { + map.set( + member.name, + isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name) + ); + } + } + + map.set('user', agentAvatarUrl('user')); + map.set('system', agentAvatarUrl('system')); + + return map; +} + +export function resolveMemberAvatarUrl( + member: MemberAvatarInput, + avatarMap?: ReadonlyMap, + size = 64 +): string { + return ( + avatarMap?.get(member.name) ?? + (isLeadMember(member) ? LEAD_PARTICIPANT_AVATAR_URL : agentAvatarUrl(member.name, size)) + ); +} + export const KANBAN_COLUMN_DISPLAY: Record< 'review' | 'approved', { label: string; bg: string; text: string }