import React, { useState } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { CARD_ICON_MUTED, CODE_BG, CODE_BORDER, COLOR_TEXT_MUTED, TOOL_CALL_BG, TOOL_CALL_BORDER, TOOL_CALL_TEXT, } from '@renderer/constants/cssVariables'; import { truncateText } from '@renderer/utils/aiGroupEnhancer'; import { formatTokensCompact } from '@renderer/utils/formatters'; import { format } from 'date-fns'; import { ChevronRight, Layers, MailOpen } from 'lucide-react'; import { MarkdownViewer } from '../viewers/MarkdownViewer'; import { BaseItem } from './BaseItem'; import { LinkedToolItem } from './LinkedToolItem'; import { TeammateMessageItem } from './TeammateMessageItem'; import { TextItem } from './TextItem'; import { ThinkingItem } from './ThinkingItem'; import type { AIGroupDisplayItem } from '@renderer/types/groups'; import type { TriggerColor } from '@shared/constants/triggerColors'; // ============================================================================= // Types // ============================================================================= interface ExecutionTraceProps { items: AIGroupDisplayItem[]; aiGroupId: string; highlightToolUseId?: string; /** Custom highlight color from trigger */ highlightColor?: TriggerColor; /** Map of tool use ID to trigger color for notification dots */ notificationColorMap?: Map; searchExpandedItemId?: string | null; /** Optional callback to register tool element refs for scroll targeting */ registerToolRef?: (toolId: string, el: HTMLDivElement | null) => void; } // ============================================================================= // Execution Trace Component // ============================================================================= export const ExecutionTrace: React.FC = React.memo( ({ items, aiGroupId: _aiGroupId, highlightToolUseId, highlightColor, notificationColorMap, searchExpandedItemId, registerToolRef, }): React.JSX.Element => { const { t } = useAppTranslation('common'); const [manualExpandedItemId, setManualExpandedItemId] = useState(null); // Use searchExpandedItemId if set, otherwise use manually expanded item const expandedItemId = searchExpandedItemId ?? manualExpandedItemId; const handleItemClick = (itemId: string): void => { setManualExpandedItemId((prev) => (prev === itemId ? null : itemId)); }; if (!items || items.length === 0) { return (
{t('chat.executionTrace.empty')}
); } return (
{items.map((item, index) => { switch (item.type) { case 'thinking': { const itemId = `subagent-thinking-${index}`; const thinkingStep = { id: itemId, type: 'thinking' as const, startTime: item.timestamp, endTime: item.timestamp, durationMs: 0, content: { thinkingText: item.content, tokenCount: item.tokenCount }, tokens: { input: 0, output: item.tokenCount ?? 0 }, context: 'subagent' as const, }; const preview = truncateText(item.content, 150); const isExpanded = expandedItemId === itemId; return ( handleItemClick(itemId)} isExpanded={isExpanded} timestamp={item.timestamp} /> ); } case 'output': { const itemId = `subagent-output-${index}`; const textStep = { id: itemId, type: 'output' as const, startTime: item.timestamp, endTime: item.timestamp, durationMs: 0, content: { outputText: item.content, tokenCount: item.tokenCount }, tokens: { input: 0, output: item.tokenCount ?? 0 }, context: 'subagent' as const, }; const preview = truncateText(item.content, 150); const isExpanded = expandedItemId === itemId; return ( handleItemClick(itemId)} isExpanded={isExpanded} timestamp={item.timestamp} /> ); } case 'tool': { const itemId = `subagent-tool-${item.tool.id}`; const isExpanded = expandedItemId === itemId; const isHighlighted = highlightToolUseId === item.tool.id; return ( handleItemClick(itemId)} isExpanded={isExpanded} timestamp={item.tool.startTime} isHighlighted={isHighlighted} highlightColor={highlightColor} notificationDotColor={notificationColorMap?.get(item.tool.id)} registerRef={ registerToolRef ? (el) => registerToolRef(item.tool.id, el) : undefined } /> ); } case 'subagent': return (
{t('chat.executionTrace.nested', { name: item.subagent.description ?? item.subagent.id, })}
); case 'subagent_input': { const itemId = `subagent-input-${index}`; const isExpanded = expandedItemId === itemId; return ( } label={t('chat.executionTrace.input')} summary={truncateText(item.content, 80)} tokenCount={item.tokenCount} timestamp={item.timestamp} onClick={() => handleItemClick(itemId)} isExpanded={isExpanded} > ); } case 'teammate_message': { const itemId = `subagent-teammate-${item.teammateMessage.id}-${index}`; const isExpanded = expandedItemId === itemId; return ( handleItemClick(itemId)} isExpanded={isExpanded} /> ); } case 'compact_boundary': { const itemId = `subagent-compact-${index}`; const isExpanded = expandedItemId === itemId; return (
{/* Header — matches CompactBoundary.tsx amber styling */} {/* Expanded content */} {isExpanded && item.content && (
)}
); } default: return null; } })}
); } ); ExecutionTrace.displayName = 'ExecutionTrace';