import React, { useCallback, useMemo, useState } from 'react'; import { CARD_BG, CARD_BORDER_STYLE, CARD_HEADER_BG, CARD_HEADER_HOVER, CARD_ICON_MUTED, CARD_SEPARATOR, CARD_TEXT_LIGHT, CARD_TEXT_LIGHTER, COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY, } from '@renderer/constants/cssVariables'; import { getSubagentTypeColorSet, getTeamColorSet } from '@renderer/constants/teamColors'; import { useTabUI } from '@renderer/hooks/useTabUI'; import { useStore } from '@renderer/store'; import { buildDisplayItemsFromMessages, buildSummary } from '@renderer/utils/aiGroupEnhancer'; import { computeSubagentPhaseBreakdown } from '@renderer/utils/aiGroupHelpers'; import { formatDuration, formatTokensCompact } from '@renderer/utils/formatters'; import { getHighlightProps, type TriggerColor } from '@shared/constants/triggerColors'; import { getModelColorClass, parseModelString } from '@shared/utils/modelParser'; import { ArrowUpRight, Bot, CheckCircle2, ChevronRight, CircleDot, Loader2, Sigma, Terminal, } from 'lucide-react'; import { ExecutionTrace } from './ExecutionTrace'; import { MetricsPill } from './MetricsPill'; import type { Process, SemanticStep } from '@renderer/types/data'; // ============================================================================= // Types // ============================================================================= interface SubagentItemProps { step: SemanticStep; subagent: Process; onClick: () => void; isExpanded: boolean; aiGroupId: string; /** Tool use ID to highlight for error deep linking */ highlightToolUseId?: string; /** Custom highlight color from trigger */ highlightColor?: TriggerColor; /** Map of tool use ID to trigger color for notification dots */ notificationColorMap?: Map; /** Optional callback to register tool element refs for scroll targeting */ registerToolRef?: (toolId: string, el: HTMLDivElement | null) => void; } // ============================================================================= // Main Component - Linear-style DevTools Card // ============================================================================= export const SubagentItem: React.FC = ({ step, subagent, onClick, isExpanded, aiGroupId, highlightToolUseId, highlightColor, notificationColorMap, registerToolRef, }) => { const description = subagent.description ?? step.content.subagentDescription ?? 'Subagent'; const subagentType = subagent.subagentType ?? 'Task'; const truncatedDesc = description.length > 60 ? description.slice(0, 60) + '...' : description; // Agent configs from .claude/agents/ for color lookup const agentConfigs = useStore((s) => s.agentConfigs); // Team member colors (when this subagent is a team member) const teamColors = subagent.team ? getTeamColorSet(subagent.team.memberColor) : null; // Type-based colors for non-team subagents (from agent config or deterministic hash) const typeColors = !teamColors ? getSubagentTypeColorSet(subagentType, agentConfigs) : null; // Detect shutdown-only team activations (trivial: just a shutdown_response) const isShutdownOnly = useMemo(() => { if (!subagent.team || !subagent.messages?.length) return false; const assistantMsgs = subagent.messages.filter((m) => m.type === 'assistant'); if (assistantMsgs.length !== 1) return false; const calls = assistantMsgs[0].toolCalls ?? []; return ( calls.length === 1 && calls[0].name === 'SendMessage' && calls[0].input?.type === 'shutdown_response' ); }, [subagent.team, subagent.messages]); // Per-tab trace expansion state (replaces local useState for true per-tab isolation) const { isSubagentTraceExpanded, toggleSubagentTraceExpansion } = useTabUI(); const isTraceManuallyExpanded = isSubagentTraceExpanded(subagent.id); // Check if contains highlighted error // Also matches when the highlight targets the parent Task tool_use that spawned this subagent const containsHighlightedError = useMemo(() => { if (!highlightToolUseId) return false; // Match parent Task tool_use ID (trigger matched the Task call itself) if (subagent.parentTaskId === highlightToolUseId) return true; // Match inner tool calls/results within the subagent if (!subagent.messages) return false; for (const msg of subagent.messages) { if (msg.toolCalls?.some((tc) => tc.id === highlightToolUseId)) return true; if (msg.toolResults?.some((tr) => tr.toolUseId === highlightToolUseId)) return true; } return false; }, [highlightToolUseId, subagent.parentTaskId, subagent.messages]); // Build display items const displayItems = useMemo(() => { if ((!isExpanded && !containsHighlightedError) || !subagent.messages?.length) { return []; } return buildDisplayItemsFromMessages(subagent.messages, []); }, [isExpanded, containsHighlightedError, subagent.messages]); // Build summary const itemsSummary = useMemo(() => { if (!isExpanded && !containsHighlightedError) { const toolCount = subagent.messages?.filter( (m) => m.type === 'assistant' && Array.isArray(m.content) && m.content.some((b) => b.type === 'tool_use') ).length ?? 0; return toolCount > 0 ? `${toolCount} tools` : ''; } return buildSummary(displayItems); }, [isExpanded, containsHighlightedError, displayItems, subagent.messages]); // Model info const modelInfo = useMemo(() => { const msg = subagent.messages?.find( (m) => m.type === 'assistant' && m.model && m.model !== '' ); return msg?.model ? parseModelString(msg.model) : null; }, [subagent.messages]); // Last usage const lastUsage = useMemo(() => { const messages = subagent.messages ?? []; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].type === 'assistant' && messages[i].usage) { return messages[i].usage; } } return null; }, [subagent.messages]); // Multi-phase context breakdown (for subagents with compaction) const phaseData = useMemo(() => { if (!subagent.messages?.length) return null; return computeSubagentPhaseBreakdown(subagent.messages); }, [subagent.messages]); // Search expansion const searchExpandedSubagentIds = useStore((s) => s.searchExpandedSubagentIds); const searchCurrentSubagentItemId = useStore((s) => s.searchCurrentSubagentItemId); const shouldExpandForSearch = searchExpandedSubagentIds.has(subagent.id); // Combine manual expansion with auto-expansion for errors/search const isTraceExpanded = isTraceManuallyExpanded || containsHighlightedError || shouldExpandForSearch; const [isTraceHeaderHovered, setIsTraceHeaderHovered] = useState(false); // Outer card highlight when this subagent contains the highlighted tool const outerHighlight = useMemo(() => { if (!containsHighlightedError) return { className: '', style: undefined as React.CSSProperties | undefined }; return getHighlightProps(highlightColor); }, [containsHighlightedError, highlightColor]); // Register outer card as a tool ref target for the parent Task tool_use ID // so the navigation controller can scroll directly to this SubagentItem const outerCardRef = useCallback( (el: HTMLDivElement | null) => { if (subagent.parentTaskId && registerToolRef) { registerToolRef(subagent.parentTaskId, el); } }, [subagent.parentTaskId, registerToolRef] ); // Cumulative metrics for team members — show total output generated const cumulativeMetrics = useMemo(() => { if (!subagent.team || !subagent.metrics) return undefined; const turnCount = subagent.messages?.filter((m) => m.type === 'assistant' && m.usage).length ?? 0; return { outputTokens: subagent.metrics.outputTokens, turnCount, }; }, [subagent.team, subagent.metrics, subagent.messages]); // Computed values for metrics const hasMainImpact = subagent.mainSessionImpact && subagent.mainSessionImpact.totalTokens > 0; const hasIsolated = lastUsage && lastUsage.input_tokens + lastUsage.output_tokens > 0; const isMultiPhase = phaseData != null && phaseData.compactionCount > 0; const isolatedTotal = isMultiPhase ? phaseData.totalConsumption : lastUsage ? lastUsage.input_tokens + lastUsage.output_tokens + (lastUsage.cache_read_input_tokens ?? 0) + (lastUsage.cache_creation_input_tokens ?? 0) : 0; // Shutdown-only team activations: minimal inline row (no metrics, no expand) if (isShutdownOnly && teamColors && subagent.team) { return (
{subagent.team.memberName} Shutdown confirmed {formatDuration(subagent.durationMs)}
); } return (
{/* ========== Level 1: Clickable Header ========== */}
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } }} className="flex cursor-pointer items-center gap-2 px-3 py-2 transition-colors" style={{ backgroundColor: isExpanded ? CARD_HEADER_BG : 'transparent', borderBottom: isExpanded ? CARD_BORDER_STYLE : 'none', }} > {/* Expand chevron */} {/* Icon - colored dot for team members/typed subagents, Bot icon for generic */} {teamColors || typeColors ? ( ) : ( )} {/* Type badge - team member name or typed subagent */} {teamColors && subagent.team ? ( {subagent.team.memberName} ) : ( {subagentType} )} {/* Model */} {modelInfo && ( {modelInfo.name} )} {/* Description */} {truncatedDesc} {/* Status indicator */} {subagent.isOngoing ? ( ) : ( )} {/* Unified Metrics Pill — team members don't show mainSessionImpact (spawn cost only; real main impact comes from teammate messages) */} 0 ? phaseData.totalConsumption : undefined } phaseBreakdown={phaseData?.phases} /> {/* Duration */} {formatDuration(subagent.durationMs)}
{/* ========== Level 1 Expanded: Dashboard Content ========== */} {isExpanded && (
{/* ========== Row 1: Meta Info (Horizontal Flow) ========== */}
Type{' '} {subagentType} Duration{' '} {formatDuration(subagent.durationMs)} {modelInfo && ( <> Model{' '} {modelInfo.name} )} ID{' '} {subagent.id.slice(0, 8)}
{/* ========== Row 2: Context Usage (Clean List) ========== */} {(hasMainImpact ?? hasIsolated) && (
{/* Overline title */}
Context Usage
{/* Token rows - floating alignment */}
{hasMainImpact && !subagent.team && (
Main Context
{subagent.mainSessionImpact!.totalTokens.toLocaleString()}
)} {cumulativeMetrics && (
Total Output
{cumulativeMetrics.outputTokens.toLocaleString()} {' '} ({cumulativeMetrics.turnCount} turns)
)} {hasIsolated && (
{subagent.team ? 'Context Window' : 'Subagent Context'}
{isolatedTotal.toLocaleString()}
)} {/* Per-phase breakdown when multi-phase */} {isMultiPhase && phaseData.phases.map((phase) => (
Phase {phase.phaseNumber} {formatTokensCompact(phase.peakTokens)} {phase.postCompaction != null && ( {' '} → {formatTokensCompact(phase.postCompaction)} )}
))}
)} {/* ========== Level 2: Execution Trace Toggle ========== */} {displayItems.length > 0 && (
{/* Trace Header (clickable) */}
{ e.stopPropagation(); toggleSubagentTraceExpansion(subagent.id); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); toggleSubagentTraceExpansion(subagent.id); } }} className="flex cursor-pointer items-center gap-2 px-3 py-2 transition-colors" style={{ borderBottom: isTraceExpanded ? CARD_BORDER_STYLE : 'none', backgroundColor: isTraceHeaderHovered ? CARD_HEADER_HOVER : 'transparent', }} onMouseEnter={() => setIsTraceHeaderHovered(true)} onMouseLeave={() => setIsTraceHeaderHovered(false)} > Execution Trace · {itemsSummary}
{/* Trace Content */} {isTraceExpanded && (
)}
)}
)}
); };