File size: 4,174 Bytes
3193174
 
 
 
 
 
 
5cdde73
 
3193174
 
 
 
 
 
 
 
 
 
 
 
 
5cdde73
 
 
3193174
 
 
 
 
 
 
 
 
 
 
 
 
 
5cdde73
3193174
 
 
 
 
 
 
 
 
 
 
 
 
5cdde73
 
 
 
 
 
 
 
 
 
 
 
3193174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import {
  Play,
  CheckCircle2,
  XCircle,
  AlertTriangle,
  Zap,
  Activity,
  StopCircle,
  GitBranch,
  type LucideIcon,
} from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { StreamEvent, StreamEventType } from "@/types/execution";

const eventConfig: Record<string, { icon: LucideIcon; color: string }> = {
  run_start: { icon: Play, color: "text-blue-500" },
  run_end: { icon: CheckCircle2, color: "text-green-500" },
  agent_start: { icon: Play, color: "text-blue-400" },
  agent_output: { icon: CheckCircle2, color: "text-green-400" },
  agent_error: { icon: XCircle, color: "text-red-500" },
  budget_warning: { icon: AlertTriangle, color: "text-yellow-500" },
  budget_exceeded: { icon: AlertTriangle, color: "text-red-500" },
  topology_changed: { icon: GitBranch, color: "text-purple-500" },
  early_stop: { icon: StopCircle, color: "text-amber-500" },
  prune: { icon: Activity, color: "text-orange-500" },
  parallel_start: { icon: Zap, color: "text-indigo-500" },
  parallel_end: { icon: Zap, color: "text-indigo-400" },
};

function formatTime(timestamp: string): string {
  try {
    const d = new Date(timestamp);
    return d.toLocaleTimeString([], { hour12: false } as Intl.DateTimeFormatOptions);
  } catch {
    return "";
  }
}

function eventSummary(event: StreamEvent): string {
  const ev = event as Record<string, any>;
  switch (event.event_type) {
    case "run_start":
      return `Run started: ${event.num_agents ?? 0} agents`;
    case "run_end":
      return `Run ${event.success ? "completed" : "failed"} in ${(event.total_time ?? 0).toFixed(1)}s (${event.total_tokens ?? 0} tokens)`;
    case "agent_start":
      return `${event.agent_name || event.agent_id} started`;
    case "agent_output": {
      const preview = (event.content || "").slice(0, 80);
      return `${event.agent_name || event.agent_id}: ${preview}${(event.content?.length ?? 0) > 80 ? "..." : ""}`;
    }
    case "agent_error":
      return `${event.agent_id} error: ${event.error_message}`;
    case "topology_changed": {
      const parts: string[] = ["Topology modified"];
      if (ev.added_edges) parts.push(`+${ev.added_edges} edges`);
      if (ev.removed_edges) parts.push(`-${ev.removed_edges} edges`);
      if (ev.skipped_agents?.length) parts.push(`skipped: ${ev.skipped_agents.join(", ")}`);
      if (ev.forced_agents?.length) parts.push(`forced: ${ev.forced_agents.join(", ")}`);
      return parts.join(" | ");
    }
    case "early_stop":
      return `Early stopped: ${ev.reason || event.content || "condition met"}`;
    case "prune":
      return `Pruned: ${ev.agent_id || ev.agents?.join(", ") || "agents"}`;
    case "parallel_start":
      return `Parallel: ${event.agent_ids?.join(", ")}`;
    case "error":
      return `Error: ${event.error || "Unknown"}`;
    case "cancelled":
      return "Execution cancelled";
    default:
      return event.event_type;
  }
}

interface ExecutionTimelineProps {
  events: StreamEvent[];
}

export function ExecutionTimeline({ events }: ExecutionTimelineProps) {
  const filtered = events.filter(
    (e) => e.event_type !== "token" && e.event_type !== "memory_read" && e.event_type !== "memory_write"
  );

  return (
    <ScrollArea className="h-full">
      <div className="space-y-1 p-2">
        {filtered.length === 0 && (
          <p className="text-xs text-muted-foreground text-center py-4">
            No events yet. Execute a workflow to see events here.
          </p>
        )}
        {filtered.map((event, i) => {
          const cfg = eventConfig[event.event_type] || { icon: Activity, color: "text-muted-foreground" };
          const Icon = cfg.icon;
          return (
            <div key={i} className="flex items-start gap-2 text-xs py-1">
              <Icon className={`h-3.5 w-3.5 mt-0.5 flex-shrink-0 ${cfg.color}`} />
              <span className="text-muted-foreground flex-shrink-0 w-16 font-mono">
                {formatTime(event.timestamp)}
              </span>
              <span className="text-foreground">{eventSummary(event)}</span>
            </div>
          );
        })}
      </div>
    </ScrollArea>
  );
}