diff --git a/src/renderer/components/team/members/MemberExecutionLog.tsx b/src/renderer/components/team/members/MemberExecutionLog.tsx index e498fbae..82a64ae8 100644 --- a/src/renderer/components/team/members/MemberExecutionLog.tsx +++ b/src/renderer/components/team/members/MemberExecutionLog.tsx @@ -4,7 +4,10 @@ import { DisplayItemList } from '@renderer/components/chat/DisplayItemList'; import { LastOutputDisplay } from '@renderer/components/chat/LastOutputDisplay'; import { SystemChatGroup } from '@renderer/components/chat/SystemChatGroup'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; +import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; +import { getTeamColorSet, getThemedBorder } from '@renderer/constants/teamColors'; +import { useTheme } from '@renderer/hooks/useTheme'; import { enhanceAIGroup } from '@renderer/utils/aiGroupEnhancer'; import { transformChunksToConversation } from '@renderer/utils/groupTransformer'; import { extractAgentBlockContents, stripAgentBlocks } from '@shared/constants/agentBlocks'; @@ -17,6 +20,8 @@ import type { AIGroup, UserGroup } from '@renderer/types/groups'; interface MemberExecutionLogProps { chunks: EnhancedChunk[]; memberName?: string; + memberColor?: string; + teamName?: string; } type ExpandedItemIdsByGroup = Map>; @@ -24,6 +29,8 @@ type ExpandedItemIdsByGroup = Map>; export const MemberExecutionLog = ({ chunks, memberName, + memberColor, + teamName, }: MemberExecutionLogProps): React.JSX.Element => { const conversation = useMemo(() => transformChunksToConversation(chunks, [], false), [chunks]); @@ -60,6 +67,8 @@ export const MemberExecutionLog = ({ key={item.group.id} group={item.group} memberName={memberName} + memberColor={memberColor} + teamName={teamName} expanded={!collapsedGroupIds.has(item.group.id)} expandedItemIds={expandedItemIdsByGroup.get(item.group.id) ?? new Set()} onToggleExpanded={() => { @@ -151,6 +160,8 @@ const UserLogItem = ({ group }: { group: UserGroup }): React.JSX.Element => { interface AIExecutionGroupProps { group: AIGroup; memberName?: string; + memberColor?: string; + teamName?: string; expanded: boolean; expandedItemIds: Set; onToggleExpanded: () => void; @@ -160,11 +171,14 @@ interface AIExecutionGroupProps { const AIExecutionGroup = ({ group, memberName, + memberColor, + teamName, expanded, expandedItemIds, onToggleExpanded, onToggleItem, }: AIExecutionGroupProps): React.JSX.Element => { + const { isLight } = useTheme(); const enhanced = useMemo(() => { if (!memberName) { return enhanceAIGroup(group); @@ -175,13 +189,18 @@ const AIExecutionGroup = ({ ); return enhanceAIGroup({ ...group, processes: filteredProcesses }); }, [group, memberName]); - const groupLabel = memberName?.trim() ? `${memberName.trim()} turn` : 'Agent turn'; + const normalizedMemberName = memberName?.trim(); + const groupLabel = normalizedMemberName ? `${normalizedMemberName} turn` : 'Agent turn'; const hasToggleContent = enhanced.displayItems.length > 0; const visibleLastOutput = enhanced.lastOutput?.type === 'tool_result' && hasToggleContent ? null : enhanced.lastOutput; + const groupColors = getTeamColorSet(memberColor ?? ''); + const borderColor = normalizedMemberName + ? getThemedBorder(groupColors, isLight) + : 'var(--chat-ai-border)'; return ( -
+
{hasToggleContent ? ( @@ -191,10 +210,27 @@ const AIExecutionGroup = ({ onClick={onToggleExpanded} aria-expanded={expanded} > - - - {groupLabel} - + {normalizedMemberName ? ( + <> + + + turn + + + ) : ( + <> + + + {groupLabel} + + + )} {enhanced.itemsSummary} diff --git a/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx b/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx index e2aba919..ec9712b6 100644 --- a/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +++ b/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx @@ -1,12 +1,25 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '@renderer/api'; +import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberExecutionLog } from '@renderer/components/team/members/MemberExecutionLog'; +import { + getTeamColorSet, + getThemedBadge, + getThemedBorder, + getThemedText, +} from '@renderer/constants/teamColors'; +import { useTheme } from '@renderer/hooks/useTheme'; +import { useStore } from '@renderer/store'; +import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { asEnhancedChunkArray } from '@renderer/types/data'; +import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; +import { isLeadMember } from '@shared/utils/leadDetection'; import { AlertCircle, Clock, FileText, Loader2 } from 'lucide-react'; import type { BoardTaskLogActor, + ResolvedTeamMember, BoardTaskLogSegment, BoardTaskLogStreamResponse, } from '@shared/types'; @@ -87,12 +100,51 @@ function describeStreamSource(stream: BoardTaskLogStreamResponse | null): string return 'Task-scoped transcript logs rendered with the same execution-log components used in Logs.'; } +interface ParticipantVisual { + name: string; + color?: string; +} + +function buildParticipantVisualMap( + stream: BoardTaskLogStreamResponse | null, + members: readonly ResolvedTeamMember[], + memberColorMap: ReadonlyMap +): Map { + const visuals = new Map(); + const leadMember = members.find((member) => isLeadMember(member)); + + for (const participant of stream?.participants ?? []) { + const matchingSegment = stream?.segments.find( + (segment) => segment.participantKey === participant.key + ); + const name = + matchingSegment?.actor.memberName ?? + (participant.isLead ? leadMember?.name : undefined) ?? + participant.label; + + visuals.set(participant.key, { + name, + color: memberColorMap.get(name) ?? memberColorMap.get(participant.label), + }); + } + + for (const segment of stream?.segments ?? []) { + if (visuals.has(segment.participantKey)) { + continue; + } + const name = segment.actor.memberName ?? actorLabel(segment.actor); + visuals.set(segment.participantKey, { + name, + color: memberColorMap.get(name), + }); + } + + return visuals; +} + const SegmentMarker = ({ segment }: { segment: BoardTaskLogSegment }): React.JSX.Element => { return ( -
- - {actorLabel(segment.actor)} - +
{formatRelativeTime(segment.endTimestamp)} @@ -104,18 +156,68 @@ const SegmentMarker = ({ segment }: { segment: BoardTaskLogSegment }): React.JSX const SegmentBlock = ({ segment, showHeader, + teamName, + visual, }: { segment: BoardTaskLogSegment; showHeader: boolean; + teamName: string; + visual?: ParticipantVisual; }): React.JSX.Element => { return (
{showHeader ? : null} - +
); }; +const ParticipantFilterChip = ({ + label, + selected, + visual, + teamName, + onClick, +}: { + label: string; + selected: boolean; + visual?: ParticipantVisual; + teamName: string; + onClick: () => void; +}): React.JSX.Element => { + const { isLight } = useTheme(); + const colors = getTeamColorSet(visual?.color ?? ''); + const borderColor = selected ? getThemedBorder(colors, isLight) : 'var(--color-border)'; + const backgroundColor = selected ? getThemedBadge(colors, isLight) : 'transparent'; + const textColor = selected ? getThemedText(colors, isLight) : 'var(--color-text-muted)'; + + return ( + + ); +}; + export const TaskLogStreamSection = ({ teamName, taskId, @@ -126,6 +228,7 @@ export const TaskLogStreamSection = ({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedParticipantKey, setSelectedParticipantKey] = useState<'all' | string>('all'); + const teamMembers = useStore((s) => selectResolvedMembersForTeamName(s, teamName)); const requestSeqRef = useRef(0); const streamRef = useRef(null); const reloadTimerRef = useRef | null>(null); @@ -277,6 +380,11 @@ export const TaskLogStreamSection = ({ }, [liveEnabled, loadStream, taskId, teamName]); const participants = stream?.participants ?? []; + const memberColorMap = useMemo(() => buildMemberColorMap(teamMembers), [teamMembers]); + const participantVisuals = useMemo( + () => buildParticipantVisualMap(stream, teamMembers, memberColorMap), + [memberColorMap, stream, teamMembers] + ); const showChips = participants.length > 1; const streamDescription = useMemo(() => describeStreamSource(stream), [stream]); const visibleSegments = useMemo(() => { @@ -340,18 +448,14 @@ export const TaskLogStreamSection = ({ All {participants.map((participant) => ( - + /> ))}
) : null} @@ -372,6 +476,8 @@ export const TaskLogStreamSection = ({ key={buildStableSegmentRenderKey(segment)} segment={segment} showHeader={showSegmentHeaders} + teamName={teamName} + visual={participantVisuals.get(segment.participantKey)} /> ))}