import { useState } from 'react'; import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, GitBranch, MessageSquare, Wrench, XCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import type { TaskStep } from './task-visualization'; interface ExecutionGraphCardProps { agentLabel: string; steps: TaskStep[]; active: boolean; /** Hide the trailing "Thinking ..." indicator even when active. */ suppressThinking?: boolean; /** * When provided, the card becomes fully controlled: the parent owns the * expand state (e.g. to persist across remounts) and toggling goes through * `onExpandedChange`. When omitted, the card manages its own local state. */ expanded?: boolean; onExpandedChange?: (expanded: boolean) => void; } const TOOL_ROW_EXTRA_INDENT_PX = 8; function AnimatedDots({ className }: { className?: string }) { return ( ); } function GraphStatusIcon({ status }: { status: TaskStep['status'] }) { if (status === 'completed') return ; if (status === 'error') return ; return ; } function StepDetailCard({ step }: { step: TaskStep }) { const { t } = useTranslation('chat'); const [expanded, setExpanded] = useState(false); const hasDetail = !!step.detail; // Narration steps (intermediate pure-text assistant messages folded from // the chat stream) are rendered without a label/status pill: the message // text IS the primary content. const isNarration = step.kind === 'message'; const isTool = step.kind === 'tool'; const isThinking = step.kind === 'thinking'; const showRunningDots = isTool && step.status === 'running'; const hideStatusText = isTool && step.status === 'completed'; const detailPreview = step.detail?.replace(/\s+/g, ' ').trim(); const canExpand = hasDetail; const usePlainExpandedDetail = isTool || isThinking; const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label; return (
{step.detail && expanded && canExpand && ( usePlainExpandedDetail ? (
            {step.detail}
          
) : (
            {step.detail}
          
) )}
); } export function ExecutionGraphCard({ agentLabel, steps, active, suppressThinking = false, expanded: controlledExpanded, onExpandedChange, }: ExecutionGraphCardProps) { const { t } = useTranslation('chat'); // Active runs should stay expanded by default so the user can follow the // execution live. Once the run completes, the default state returns to // collapsed. Explicit user toggles remain controlled by the parent override. const [uncontrolledExpanded, setUncontrolledExpanded] = useState(active); const [prevActive, setPrevActive] = useState(active); if (prevActive !== active) { setPrevActive(active); if (controlledExpanded == null && uncontrolledExpanded !== active) { setUncontrolledExpanded(active); } } const isControlled = controlledExpanded != null; const expanded = isControlled ? controlledExpanded : uncontrolledExpanded; const setExpanded = (next: boolean) => { if (!isControlled) setUncontrolledExpanded(next); onExpandedChange?.(next); }; const toolCount = steps.filter((step) => step.kind === 'tool').length; const processCount = steps.length - toolCount; const shouldShowTrailingThinking = active && !suppressThinking; if (!expanded) { return ( ); } return (
{t('executionGraph.agentRun', { agent: agentLabel })}
{steps.map((step) => { const alignedIndentOffset = ( step.kind === 'tool' || step.kind === 'message' || step.kind === 'thinking' ) ? TOOL_ROW_EXTRA_INDENT_PX : 0; const rowMarginLeft = (Math.max(step.depth - 1, 0) * 24) + alignedIndentOffset; return (
{step.depth > 1 && (
)}
{step.kind === 'thinking' ? : step.kind === 'tool' ? : step.kind === 'message' ? : }
)})} {shouldShowTrailingThinking && (
{t('executionGraph.thinkingLabel')}
)}
); }