'use client'; import * as React from 'react'; import { cn } from '../../lib/utils'; import { StatusBadge } from '../badge/StatusBadge'; import { EmptyState } from '../states/EmptyState'; import type { NodeExecView } from './types'; import { NODE_STATUS_BADGE, formatDuration, isLiveStatus } from './exec-status'; export interface ExecutionTimelineProps { /** Per-node execution overlays; rendered sorted by `executionOrder`. */ nodeExecutions: NodeExecView[]; /** Currently selected node id (row highlighted as active). */ activeNodeId?: string; /** Called with a node id when its row is activated. */ onSelectNode?: (nodeId: string) => void; className?: string; } function byExecutionOrder(a: NodeExecView, b: NodeExecView): number { const ao = a.executionOrder ?? 0; const bo = b.executionOrder ?? 0; return ao - bo; } /** * Vertical execution timeline: one row per node execution (ordered by * `executionOrder`), each showing the node name, a status badge, the run * duration, and inline error text for failed nodes. Running nodes show a * pulsing live indicator; terminal nodes render statically. * * Read-only and app-agnostic — selection is surfaced via `onSelectNode`. */ export function ExecutionTimeline({ nodeExecutions, activeNodeId, onSelectNode, className, }: ExecutionTimelineProps) { const ordered = React.useMemo( () => [...nodeExecutions].sort(byExecutionOrder), [nodeExecutions] ); if (ordered.length === 0) { return ( ); } return (
    {ordered.map((node) => { const live = isLiveStatus(node.status); const isActive = activeNodeId != null && node.nodeId === activeNodeId; const duration = formatDuration(node.executionTimeMs); const selectable = onSelectNode != null; return (
  1. onSelectNode!(node.nodeId) : undefined} onKeyDown={ selectable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelectNode!(node.nodeId); } } : undefined } tabIndex={selectable ? 0 : undefined} > {/* Status rail dot */} {live ? ( <>
  2. ); })}
); }