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}
) : (
)
)}
);
}
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')}
)}
);
}