@elizaos/client 1.6.1-alpha.4 → 1.6.1-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/assets/{main-C4q5_rtN.js → main-4tyUgNqd.js} +3 -3
  2. package/dist/assets/{main-C4q5_rtN.js.map → main-4tyUgNqd.js.map} +1 -1
  3. package/dist/assets/{main-BNtEiK3o.js → main-Bbs84AcL.js} +77 -63
  4. package/dist/assets/main-Bbs84AcL.js.map +1 -0
  5. package/dist/assets/{main-BOBWcKWW.css → main-CNv6B3RZ.css} +597 -71
  6. package/dist/assets/react-vendor-DxnAFk-d.js +611 -0
  7. package/dist/assets/react-vendor-DxnAFk-d.js.map +1 -0
  8. package/dist/index.html +1 -1
  9. package/package.json +8 -4
  10. package/src/components/agent-prism/Avatar.tsx +164 -0
  11. package/src/components/agent-prism/Badge.tsx +109 -0
  12. package/src/components/agent-prism/Button.tsx +138 -0
  13. package/src/components/agent-prism/CollapseAndExpandControls.tsx +45 -0
  14. package/src/components/agent-prism/CollapsibleSection.tsx +121 -0
  15. package/src/components/agent-prism/DetailsView/DetailsView.tsx +141 -0
  16. package/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx +45 -0
  17. package/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx +77 -0
  18. package/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx +21 -0
  19. package/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx +210 -0
  20. package/src/components/agent-prism/DetailsView/DetailsViewMetrics.tsx +53 -0
  21. package/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx +24 -0
  22. package/src/components/agent-prism/IconButton.tsx +75 -0
  23. package/src/components/agent-prism/PriceBadge.tsx +12 -0
  24. package/src/components/agent-prism/SearchInput.tsx +17 -0
  25. package/src/components/agent-prism/SpanCard/SpanCard.tsx +467 -0
  26. package/src/components/agent-prism/SpanCard/SpanCardBadges.tsx +35 -0
  27. package/src/components/agent-prism/SpanCard/SpanCardConnector.tsx +36 -0
  28. package/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx +60 -0
  29. package/src/components/agent-prism/SpanCard/SpanCardToggle.tsx +32 -0
  30. package/src/components/agent-prism/SpanStatus.tsx +79 -0
  31. package/src/components/agent-prism/Tabs.tsx +141 -0
  32. package/src/components/agent-prism/TextInput.tsx +142 -0
  33. package/src/components/agent-prism/TimestampBadge.tsx +28 -0
  34. package/src/components/agent-prism/TokensBadge.tsx +26 -0
  35. package/src/components/agent-prism/TraceList/TraceList.tsx +80 -0
  36. package/src/components/agent-prism/TraceList/TraceListItem.tsx +79 -0
  37. package/src/components/agent-prism/TraceList/TraceListItemHeader.tsx +46 -0
  38. package/src/components/agent-prism/TraceViewer.tsx +476 -0
  39. package/src/components/agent-prism/TreeView.tsx +57 -0
  40. package/src/components/agent-prism/shared.ts +210 -0
  41. package/src/components/agent-runs/AgentRunTimeline.tsx +64 -673
  42. package/src/components/agent-sidebar.tsx +2 -2
  43. package/src/components/chat.tsx +8 -8
  44. package/src/lib/agent-prism-utils.ts +46 -0
  45. package/src/lib/eliza-span-adapter.ts +487 -0
  46. package/dist/assets/main-BNtEiK3o.js.map +0 -1
  47. package/dist/assets/react-vendor-pe76PXQl.js +0 -546
  48. package/dist/assets/react-vendor-pe76PXQl.js.map +0 -1
@@ -1,700 +1,91 @@
1
- import { useAgentRunDetail, useAgentRuns } from '@/hooks/use-query-hooks';
2
- import { cn } from '@/lib/utils';
1
+ import { useAgentRuns } from '@/hooks/use-query-hooks';
3
2
  import type { UUID } from '@elizaos/core';
4
- import {
5
- Activity,
6
- AlertCircle,
7
- CheckCircle,
8
- ChevronDown,
9
- ChevronRight,
10
- Clock,
11
- Database,
12
- Eye,
13
- XCircle,
14
- Zap,
15
- } from 'lucide-react';
16
- import React, { useMemo, useState } from 'react';
3
+ import React, { useMemo } from 'react';
4
+ import { elizaSpanAdapter } from '@/lib/eliza-span-adapter';
5
+ import { Loader2 } from 'lucide-react';
6
+ import { useQueries } from '@tanstack/react-query';
7
+ import { createElizaClient } from '@/lib/api-client-config';
8
+ import type { RunDetail } from '@elizaos/api-client';
9
+ import { TraceViewer, type TraceViewerData } from '../agent-prism/TraceViewer';
17
10
 
18
11
  type AgentRunTimelineProps = {
19
12
  agentId: UUID;
20
13
  };
21
14
 
22
- type RunStatus = 'completed' | 'started' | 'timeout' | 'error';
23
-
24
- interface TimelineEvent {
25
- id: string;
26
- type:
27
- | 'RUN_STARTED'
28
- | 'RUN_ENDED'
29
- | 'ACTION_STARTED'
30
- | 'ACTION_COMPLETED'
31
- | 'MODEL_USED'
32
- | 'EVALUATOR_COMPLETED'
33
- | 'EMBEDDING_EVENT';
34
- timestamp: number;
35
- duration?: number;
36
- data: Record<string, unknown>;
37
- parentId?: string;
38
- }
39
-
40
- interface ProcessedRun {
41
- id: string;
42
- name: string;
43
- status: RunStatus;
44
- startTime: number;
45
- endTime?: number;
46
- duration?: number;
47
- children: ProcessedEvent[];
48
- counts: {
49
- actions: number;
50
- modelCalls: number;
51
- errors: number;
52
- evaluators: number;
53
- };
54
- }
55
-
56
- interface ProcessedEvent {
57
- id: string;
58
- name: string;
59
- type: 'action' | 'attempt' | 'model' | 'evaluator' | 'embedding';
60
- status: 'completed' | 'failed' | 'running';
61
- startTime: number;
62
- duration?: number;
63
- icon: React.ComponentType<{ className?: string }>;
64
- attempts?: ProcessedEvent[];
65
- children?: ProcessedEvent[];
66
- }
67
-
68
- function formatDuration(durationMs?: number | null): string {
69
- if (!durationMs || durationMs < 0) return '—';
70
- if (durationMs < 1000) return `${durationMs.toFixed(2)}ms`;
71
- const seconds = (durationMs / 1000).toFixed(2);
72
- return `${seconds}s`;
73
- }
74
-
75
- function formatTime(timestamp: number): string {
76
- return new Date(timestamp).toLocaleTimeString('en-US', {
77
- hour12: false,
78
- hour: '2-digit',
79
- minute: '2-digit',
80
- second: '2-digit',
81
- fractionalSecondDigits: 3,
82
- });
83
- }
84
-
85
- function getStatusIcon(status: string) {
86
- switch (status) {
87
- case 'completed':
88
- return CheckCircle;
89
- case 'error':
90
- case 'failed':
91
- return XCircle;
92
- case 'timeout':
93
- return AlertCircle;
94
- case 'running':
95
- case 'started':
96
- return Clock;
97
- default:
98
- return Activity;
99
- }
100
- }
101
-
102
- function getStatusColor(status: string): string {
103
- switch (status) {
104
- case 'completed':
105
- return 'text-green-700 bg-green-100 border-green-300 dark:text-green-400 dark:bg-green-900/30 dark:border-green-800';
106
- case 'error':
107
- case 'failed':
108
- return 'text-red-700 bg-red-100 border-red-300 dark:text-red-400 dark:bg-red-900/30 dark:border-red-800';
109
- case 'timeout':
110
- return 'text-yellow-700 bg-yellow-100 border-yellow-300 dark:text-yellow-400 dark:bg-yellow-900/30 dark:border-yellow-800';
111
- case 'running':
112
- case 'started':
113
- return 'text-blue-700 bg-blue-100 border-blue-300 dark:text-blue-400 dark:bg-blue-900/30 dark:border-blue-800';
114
- default:
115
- return 'text-muted-foreground bg-muted border-border';
116
- }
117
- }
118
-
119
- function getEventIcon(type: string) {
120
- switch (type) {
121
- case 'action':
122
- return Activity;
123
- case 'attempt':
124
- return Clock;
125
- case 'model':
126
- return Eye;
127
- case 'evaluator':
128
- return Database;
129
- case 'embedding':
130
- return Zap;
131
- default:
132
- return Activity;
133
- }
134
- }
135
-
136
- // Removed page-level time scale to avoid confusing global durations across runs
15
+ const elizaClient = createElizaClient();
137
16
 
138
17
  export const AgentRunTimeline: React.FC<AgentRunTimelineProps> = ({ agentId }) => {
139
- const [selectedRunId, setSelectedRunId] = useState<UUID | null>(null);
140
- const [expandedRuns, setExpandedRuns] = useState<Set<string>>(new Set());
141
-
142
18
  const runsQuery = useAgentRuns(agentId);
143
19
  const runs = runsQuery.data?.runs ?? [];
144
20
 
145
- // Auto-select first run
146
- React.useEffect(() => {
147
- if (!selectedRunId && runs.length > 0) {
148
- setSelectedRunId(runs[0].runId);
149
- }
150
- }, [runs, selectedRunId]);
151
-
152
- const runDetailQuery = useAgentRunDetail(agentId, selectedRunId);
153
-
154
- const selectedRunSummary = useMemo(
155
- () => runs.find((run) => run.runId === selectedRunId) ?? runs[0] ?? null,
156
- [runs, selectedRunId]
157
- );
158
-
159
- // Process run data into hierarchical structure
160
- const processedRuns = useMemo((): ProcessedRun[] => {
161
- if (runs.length === 0) {
162
- return [];
163
- }
164
-
165
- return runs.map((runSummary) => {
166
- // For now, create a processed run from the summary data
167
- // We'll only show detailed events for the selected run
168
- const isSelected = runSummary.runId === selectedRunId;
169
- const runDetail = isSelected ? runDetailQuery.data : null;
170
-
171
- let processedEvents: ProcessedEvent[] = [];
172
-
173
- if (runDetail && runDetail.events) {
174
- // Build grouped actions with attempts and nested events
175
- const orderedEvents: ProcessedEvent[] = [];
176
- const actionMap = new Map<string, ProcessedEvent>();
177
- const inFlightAttempt = new Map<string, ProcessedEvent>();
178
-
179
- const eventsSorted = [...runDetail.events].sort((a, b) => a.timestamp - b.timestamp);
180
-
181
- eventsSorted.forEach((event, index) => {
182
- switch (event.type) {
183
- case 'ACTION_STARTED': {
184
- const actionName =
185
- (event.data.actionName as string) ||
186
- (event.data.actionId as string) ||
187
- `Action ${index}`;
188
- const actionKey = (event.data.actionId as string) || actionName;
189
-
190
- let actionEvent = actionMap.get(actionKey);
191
- if (!actionEvent) {
192
- actionEvent = {
193
- id: `action-${actionKey}`,
194
- name: actionName,
195
- type: 'action',
196
- status: 'running',
197
- startTime: event.timestamp,
198
- icon: Activity,
199
- attempts: [],
200
- };
201
- actionMap.set(actionKey, actionEvent);
202
- orderedEvents.push(actionEvent);
203
- } else {
204
- // For multiple attempts, keep earliest start
205
- actionEvent.startTime = Math.min(actionEvent.startTime, event.timestamp);
206
- actionEvent.status = 'running';
207
- }
208
-
209
- const attemptIndex = (actionEvent.attempts?.length || 0) + 1;
210
- const attemptEvent: ProcessedEvent = {
211
- id: `attempt-${actionKey}-${attemptIndex}`,
212
- name: `Attempt ${attemptIndex}`,
213
- type: 'attempt',
214
- status: 'running',
215
- startTime: event.timestamp,
216
- icon: Clock,
217
- attempts: [], // will hold nested child events like model calls
218
- };
219
- actionEvent.attempts = [...(actionEvent.attempts || []), attemptEvent];
220
- inFlightAttempt.set(actionKey, attemptEvent);
221
- break;
222
- }
223
- case 'ACTION_COMPLETED': {
224
- const actionName =
225
- (event.data.actionName as string) ||
226
- (event.data.actionId as string) ||
227
- `Action ${index}`;
228
- const actionKey = (event.data.actionId as string) || actionName;
229
- let actionEvent = actionMap.get(actionKey);
230
- if (!actionEvent) {
231
- // If we missed the start, create a placeholder action with a single attempt
232
- actionEvent = {
233
- id: `action-${actionKey}`,
234
- name: actionName,
235
- type: 'action',
236
- status: 'running',
237
- startTime: event.timestamp,
238
- icon: Activity,
239
- attempts: [],
240
- };
241
- actionMap.set(actionKey, actionEvent);
242
- orderedEvents.push(actionEvent);
243
- }
244
-
245
- let attempt = inFlightAttempt.get(actionKey);
246
- if (!attempt) {
247
- // Missing start; synthesize an attempt starting at completion time
248
- attempt = {
249
- id: `attempt-${actionKey}-1`,
250
- name: 'Attempt 1',
251
- type: 'attempt',
252
- status: 'running',
253
- startTime: event.timestamp,
254
- icon: Clock,
255
- attempts: [],
256
- };
257
- actionEvent.attempts = [...(actionEvent.attempts || []), attempt];
258
- }
259
-
260
- const success = (event.data.success as boolean | undefined) !== false;
261
- attempt.duration = Math.max(0, event.timestamp - attempt.startTime);
262
- attempt.status = success ? 'completed' : 'failed';
263
- inFlightAttempt.delete(actionKey);
264
-
265
- actionEvent.status = success ? 'completed' : 'failed';
266
- const firstAttemptStart = (actionEvent.attempts || [attempt])[0].startTime;
267
- actionEvent.duration = Math.max(0, event.timestamp - firstAttemptStart);
268
- break;
269
- }
270
- case 'MODEL_USED': {
271
- const modelType = (event.data.modelType as string) || 'Model Call';
272
- const modelEvent: ProcessedEvent = {
273
- id: `model-${index}`,
274
- name: modelType,
275
- type: 'model',
276
- status: 'completed',
277
- startTime: event.timestamp,
278
- duration: (event.data.executionTime as number) || undefined,
279
- icon: Eye,
280
- };
281
- const actionContext = (event.data.actionContext as string | undefined) || undefined;
282
- const targetKey = actionContext || Array.from(inFlightAttempt.keys()).pop();
283
- if (targetKey) {
284
- const attempt = inFlightAttempt.get(targetKey);
285
- if (attempt) {
286
- attempt.attempts = [...(attempt.attempts || []), modelEvent];
287
- } else {
288
- // If no running attempt, attach to the last attempt of the action
289
- const actionEvent = actionMap.get(targetKey);
290
- const lastAttempt = actionEvent?.attempts && actionEvent.attempts[actionEvent.attempts.length - 1];
291
- if (lastAttempt) {
292
- lastAttempt.attempts = [...(lastAttempt.attempts || []), modelEvent];
293
- } else {
294
- orderedEvents.push(modelEvent);
295
- }
296
- }
297
- } else {
298
- orderedEvents.push(modelEvent);
299
- }
300
- break;
301
- }
302
- case 'EVALUATOR_COMPLETED': {
303
- const evaluatorName = (event.data.evaluatorName as string) || `Evaluator ${index}`;
304
- orderedEvents.push({
305
- id: `evaluator-${index}`,
306
- name: evaluatorName,
307
- type: 'evaluator',
308
- status: 'completed',
309
- startTime: event.timestamp,
310
- icon: Database,
311
- });
312
- break;
313
- }
314
- case 'EMBEDDING_EVENT': {
315
- const status = (event.data.status as string) || 'completed';
316
- const embeddingEvent: ProcessedEvent = {
317
- id: `embedding-${index}`,
318
- name: `Embedding ${status}`,
319
- type: 'embedding',
320
- status: status === 'failed' ? 'failed' : 'completed',
321
- startTime: event.timestamp,
322
- duration: (event.data.durationMs as number) || undefined,
323
- icon: Zap,
324
- };
325
- const key = Array.from(inFlightAttempt.keys()).pop();
326
- if (key) {
327
- const attempt = inFlightAttempt.get(key);
328
- if (attempt) {
329
- attempt.attempts = [...(attempt.attempts || []), embeddingEvent];
330
- } else {
331
- orderedEvents.push(embeddingEvent);
332
- }
333
- } else {
334
- orderedEvents.push(embeddingEvent);
335
- }
336
- break;
337
- }
338
- default:
339
- break;
340
- }
341
- });
342
-
343
- processedEvents = orderedEvents;
344
- }
345
-
346
- return {
347
- id: runSummary.runId,
348
- name: `Run ${formatTime(runSummary.startedAt || Date.now())}`,
349
- status: runSummary.status as RunStatus,
350
- startTime: runSummary.startedAt || Date.now(),
351
- endTime: runSummary.endedAt || undefined,
352
- duration:
353
- runSummary.durationMs ??
354
- (runSummary.endedAt != null && runSummary.startedAt != null
355
- ? runSummary.endedAt - runSummary.startedAt
356
- : undefined),
357
- children: processedEvents.sort((a, b) => a.startTime - b.startTime),
358
- counts: runSummary.counts || { actions: 0, modelCalls: 0, errors: 0, evaluators: 0 },
359
- };
360
- });
361
- }, [runs, runDetailQuery.data, selectedRunId]);
362
-
363
- // Helper function to calculate timeline bounds for a single run
364
- const calculateRunTimelineBounds = (run: ProcessedRun) => {
365
- let earliestStart = run.startTime;
366
- let latestEnd = run.endTime || run.startTime + (run.duration || 0);
367
-
368
- const scanEvent = (ev: ProcessedEvent) => {
369
- earliestStart = Math.min(earliestStart, ev.startTime);
370
- const end = ev.startTime + (ev.duration || 0);
371
- latestEnd = Math.max(latestEnd, end);
372
- (ev.attempts || []).forEach(scanEvent);
373
- (ev.children || []).forEach(scanEvent);
374
- };
375
-
376
- run.children.forEach(scanEvent);
377
-
378
- const totalDuration = latestEnd - earliestStart;
379
-
380
- // Ensure we have at least a minimal duration for visualization
381
- const minDuration = 100; // 100ms minimum for visualization
382
-
383
- return {
384
- startTime: earliestStart,
385
- endTime: latestEnd,
386
- totalDuration: Math.max(totalDuration, minDuration),
387
- };
388
- };
389
-
390
- // Toggle expansion of runs
391
- const toggleRunExpansion = (runId: string) => {
392
- const newExpanded = new Set(expandedRuns);
393
- if (newExpanded.has(runId)) {
394
- newExpanded.delete(runId);
395
- } else {
396
- newExpanded.add(runId);
397
- }
398
- setExpandedRuns(newExpanded);
399
- };
21
+ // Fetch details for all runs using useQueries to avoid hook rule violations
22
+ const runDetailQueries = useQueries({
23
+ queries: runs.map((run) => ({
24
+ queryKey: ['agent', agentId, 'runs', 'detail', run.runId, null],
25
+ queryFn: async () => elizaClient.runs.getRun(agentId, run.runId),
26
+ enabled: Boolean(agentId && run.runId),
27
+ staleTime: 30000,
28
+ })),
29
+ });
400
30
 
401
- const isLoading = runsQuery.isLoading || runDetailQuery.isLoading;
31
+ // Convert ElizaOS runs to Agent Prism format
32
+ const traceViewerData: TraceViewerData[] = useMemo(() => {
33
+ return runs
34
+ .map((run, index) => {
35
+ const detailQuery = runDetailQueries[index];
36
+ if (!detailQuery?.data) return null;
37
+
38
+ return {
39
+ traceRecord: elizaSpanAdapter.convertRunSummaryToTraceRecord(run),
40
+ spans: elizaSpanAdapter.convertRunDetailToTraceSpans(detailQuery.data as RunDetail),
41
+ };
42
+ })
43
+ .filter((item): item is TraceViewerData => item !== null);
44
+ }, [runs, ...runDetailQueries.map(q => q.data)]);
45
+
46
+ const isLoading = runsQuery.isLoading;
402
47
  const errorMessage = runsQuery.error ? (runsQuery.error as Error).message : undefined;
403
48
  const hasRuns = runs.length > 0;
49
+ const hasData = traceViewerData.length > 0;
404
50
 
405
- return (
406
- <div className="space-y-4">
407
- <div className="flex flex-wrap items-center justify-between gap-4">
408
- <div>
409
- <h2 className="text-lg font-semibold">Agent Runs</h2>
410
- <p className="text-sm text-muted-foreground">
411
- Hierarchical view of agent execution with timing details.
412
- </p>
413
- </div>
414
- {/* Intentionally no global time scale; each row shows its own proportional bar */}
51
+ if (isLoading) {
52
+ return (
53
+ <div className="flex items-center justify-center h-32">
54
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
415
55
  </div>
56
+ );
57
+ }
416
58
 
417
- {!isLoading && errorMessage && (
418
- <div className="px-4 py-8 text-sm text-red-500">Failed to load runs: {errorMessage}</div>
419
- )}
420
- {!isLoading && !errorMessage && !hasRuns && (
421
- <div className="px-4 py-8 text-sm text-muted-foreground">No runs available yet.</div>
422
- )}
423
-
424
- {processedRuns.length > 0 && (
425
- <div className="space-y-2">
426
- {processedRuns.map((run) => (
427
- <div key={run.id}>
428
- <RunItem
429
- run={run}
430
- isExpanded={expandedRuns.has(run.id)}
431
- isSelected={selectedRunId === run.id}
432
- onToggle={() => {
433
- toggleRunExpansion(run.id);
434
- // Also set this as the selected run when expanded
435
- if (!expandedRuns.has(run.id)) {
436
- setSelectedRunId(run.id as UUID);
437
- }
438
- }}
439
- level={0}
440
- timelineBounds={calculateRunTimelineBounds(run)}
441
- />
442
- </div>
443
- ))}
444
- </div>
445
- )}
446
- </div>
447
- );
448
- };
449
-
450
- // RunItem component for hierarchical display
451
- interface RunItemProps {
452
- run: ProcessedRun;
453
- isExpanded: boolean;
454
- onToggle: () => void;
455
- level: number;
456
- isSelected?: boolean;
457
- timelineBounds: {
458
- startTime: number;
459
- endTime: number;
460
- totalDuration: number;
461
- };
462
- }
463
-
464
- const RunItem: React.FC<RunItemProps> = ({
465
- run,
466
- isExpanded,
467
- onToggle,
468
- level,
469
- isSelected,
470
- timelineBounds,
471
- }) => {
472
- const StatusIcon = getStatusIcon(run.status);
473
- const indent = level * 24;
474
-
475
- // Calculate timing bar parameters based on timeline bounds
476
- // For the run itself, we want the bar to span the full width since it represents the entire timeline
477
- const { startTime: timelineStart, totalDuration: timelineTotal } = timelineBounds;
478
- const runDuration = run.duration || 0;
479
-
480
- // For a run at the top level, the bar should show the full duration
481
- const isRootRun = level === 0;
482
- const startOffset = isRootRun ? 0 :
483
- (timelineTotal > 0 ? ((run.startTime - timelineStart) / timelineTotal) * 100 : 0);
484
- const widthPercent = isRootRun ? 100 :
485
- (timelineTotal > 0 ? (runDuration / timelineTotal) * 100 : 0);
486
-
487
- return (
488
- <div>
489
- {/* Main run row */}
490
- <div
491
- className={cn(
492
- 'flex items-center gap-3 p-3 cursor-pointer transition-colors',
493
- isRootRun ? 'bg-card border rounded-lg' : '',
494
- isSelected && !isRootRun && 'bg-primary/10'
495
- )}
496
- style={{ paddingLeft: `${12 + indent}px` }}
497
- onClick={onToggle}
498
- >
499
- {/* Expand/collapse button */}
500
- <button className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
501
- {run.children.length > 0 &&
502
- (isExpanded ? (
503
- <ChevronDown className="w-3 h-3" />
504
- ) : (
505
- <ChevronRight className="w-3 h-3" />
506
- ))}
507
- </button>
508
-
509
- {/* Status icon */}
510
- <div
511
- className={cn(
512
- 'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs',
513
- getStatusColor(run.status)
514
- )}
515
- >
516
- <StatusIcon className="w-3 h-3" />
517
- </div>
518
-
519
- {/* Task name and details */}
520
- <div className="flex-1 min-w-0">
521
- <div className="flex items-center gap-2">
522
- <span className="font-medium text-sm">{run.name}</span>
523
- <span
524
- className={cn(
525
- 'px-1.5 py-0.5 text-xs rounded-full border',
526
- getStatusColor(run.status)
527
- )}
528
- >
529
- {run.status === 'completed' ? '✓' : run.status === 'error' ? '✗' : '○'}
530
- </span>
531
- </div>
532
- <div className="text-xs text-muted-foreground mt-1">
533
- {run.counts.actions} actions • {run.counts.modelCalls} model calls • {run.counts.errors}{' '}
534
- errors
535
- </div>
536
- </div>
537
-
538
- {/* Timing bar with integrated duration display */}
539
- <div className="flex-shrink-0 w-40 h-5 relative bg-muted rounded-none overflow-hidden">
540
- <div
541
- className={cn(
542
- 'absolute top-0 bottom-0 transition-all flex items-center justify-center',
543
- run.status === 'completed'
544
- ? 'bg-primary'
545
- : run.status === 'error'
546
- ? 'bg-destructive'
547
- : run.status === 'timeout'
548
- ? 'bg-yellow-500 dark:bg-yellow-600'
549
- : 'bg-primary/70'
550
- )}
551
- style={{
552
- left: `${Math.max(0, Math.min(startOffset, 98))}%`,
553
- width: `${Math.max(2, Math.min(widthPercent, 100 - Math.max(0, Math.min(startOffset, 98))))}%`,
554
- }}
555
- >
556
- {/* Show duration on the bar for parent runs */}
557
- {isRootRun && (
558
- <span className="text-xs font-mono text-primary-foreground px-1 truncate">
559
- {formatDuration(run.duration)}
560
- </span>
561
- )}
562
- </div>
563
- {/* Show duration outside for child runs */}
564
- {!isRootRun && (
565
- <div className="absolute right-1 top-0 bottom-0 flex items-center">
566
- <span className="text-xs font-mono text-muted-foreground">
567
- {formatDuration(run.duration)}
568
- </span>
569
- </div>
570
- )}
571
- </div>
59
+ if (errorMessage) {
60
+ return (
61
+ <div className="px-4 py-3 text-sm text-destructive">
62
+ Failed to load runs: {errorMessage}
572
63
  </div>
573
-
574
- {/* Children */}
575
- {isExpanded && run.children.length > 0 && (
576
- <div className={cn(
577
- "border-l border-border ml-4",
578
- isRootRun ? "mt-2 pt-2" : ""
579
- )}>
580
- {run.children.map((child) => (
581
- <EventItem
582
- key={child.id}
583
- event={child}
584
- level={level + 1}
585
- timelineBounds={timelineBounds}
586
- />
587
- ))}
588
- </div>
589
- )}
590
- </div>
591
- );
592
- };
593
-
594
- // EventItem component for individual events
595
- interface EventItemProps {
596
- event: ProcessedEvent;
597
- level: number;
598
- timelineBounds: {
599
- startTime: number;
600
- endTime: number;
601
- totalDuration: number;
602
- };
603
- }
604
-
605
- const EventItem: React.FC<EventItemProps> = ({ event, level, timelineBounds }) => {
606
- const IconComponent = event.icon;
607
- const StatusIcon = getStatusIcon(event.status);
608
- const indent = level * 24;
609
- const [expanded, setExpanded] = React.useState(true);
610
- const hasNested = (event.attempts && event.attempts.length > 0) || (event.children && event.children.length > 0);
611
-
612
- // Calculate timing bar parameters based on timeline bounds
613
- const { startTime: timelineStart, totalDuration: timelineTotal } = timelineBounds;
614
- const eventDuration = event.duration || 0;
615
- const startOffset =
616
- timelineTotal > 0 ? ((event.startTime - timelineStart) / timelineTotal) * 100 : 0;
617
-
618
- // Calculate width with a minimum visible size
619
- // Use logarithmic scaling for very small durations to maintain visibility
620
- let widthPercent = 0;
621
- if (timelineTotal > 0 && eventDuration > 0) {
622
- const rawPercent = (eventDuration / timelineTotal) * 100;
623
- // Ensure minimum visibility of 5% for any event with duration
624
- widthPercent = Math.max(5, rawPercent);
625
- // For very small events (< 2%), apply scaling to make them more visible
626
- if (rawPercent < 2) {
627
- widthPercent = 3 + (rawPercent * 2); // Scale up small events
628
- }
64
+ );
629
65
  }
630
66
 
631
- return (
632
- <div>
633
- <div
634
- className="flex items-center gap-3 p-2 text-sm hover:bg-muted/30"
635
- style={{ paddingLeft: `${12 + indent}px` }}
636
- >
637
- {/* Expand/collapse if nested */}
638
- <button className="w-4 h-4 flex items-center justify-center" onClick={() => hasNested && setExpanded(!expanded)}>
639
- {hasNested ? (expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />) : null}
640
- </button>
641
-
642
- {/* Event icon */}
643
- <div className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
644
- <IconComponent className="w-3 h-3 text-muted-foreground" />
645
- </div>
646
-
647
- {/* Status icon */}
648
- <div
649
- className={cn(
650
- 'flex-shrink-0 w-4 h-4 rounded-sm flex items-center justify-center text-xs',
651
- getStatusColor(event.status)
652
- )}
653
- >
654
- <StatusIcon className="w-2.5 h-2.5" />
655
- </div>
656
-
657
- {/* Event name */}
658
- <div className="flex-1 min-w-0">
659
- <span className="text-sm">{event.name}</span>
660
- </div>
67
+ if (!hasRuns) {
68
+ return (
69
+ <div className="px-4 py-8 text-sm text-center text-muted-foreground">
70
+ No agent runs yet. Runs will appear here after the agent processes messages.
71
+ </div>
72
+ );
73
+ }
661
74
 
662
- {/* Mini timing bar with duration */}
663
- <div className="flex-shrink-0 w-40 h-3 relative bg-muted rounded-none overflow-hidden flex items-center">
664
- <div
665
- className={cn(
666
- 'absolute h-full transition-all',
667
- event.status === 'completed'
668
- ? 'bg-primary/80'
669
- : event.status === 'failed'
670
- ? 'bg-destructive/80'
671
- : 'bg-primary/60'
672
- )}
673
- style={{
674
- left: `${Math.max(0, Math.min(startOffset, 98))}%`,
675
- width: `${Math.max(3, Math.min(widthPercent, 100 - Math.max(0, Math.min(startOffset, 98))))}%`,
676
- }}
677
- />
678
- {/* Duration text overlay */}
679
- <div className="absolute right-1 text-xs font-mono text-muted-foreground z-10">
680
- {formatDuration(event.duration)}
681
- </div>
682
- </div>
75
+ if (!hasData) {
76
+ return (
77
+ <div className="flex items-center justify-center h-32">
78
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
79
+ <span className="ml-2 text-sm text-muted-foreground">Loading run details...</span>
683
80
  </div>
81
+ );
82
+ }
684
83
 
685
- {/* Nested attempts or children */}
686
- {hasNested && expanded && (
687
- <div className="border-l border-border ml-4">
688
- {(event.attempts || []).map((child) => (
689
- <EventItem key={child.id} event={child} level={level + 1} timelineBounds={timelineBounds} />
690
- ))}
691
- {(event.children || []).map((child) => (
692
- <EventItem key={child.id} event={child} level={level + 1} timelineBounds={timelineBounds} />
693
- ))}
694
- </div>
695
- )}
84
+ return (
85
+ <div className="h-full w-full">
86
+ <TraceViewer data={traceViewerData} />
696
87
  </div>
697
88
  );
698
89
  };
699
90
 
700
- export default AgentRunTimeline;
91
+ export default AgentRunTimeline;