@elizaos/client 1.6.0-beta.0 → 1.6.1-alpha.1
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.
- package/dist/assets/main-BNtEiK3o.js +141 -0
- package/dist/assets/main-BNtEiK3o.js.map +1 -0
- package/dist/assets/{main-CkbfKgca.css → main-BOBWcKWW.css} +119 -1
- package/dist/assets/{main--sdaD56A.js → main-C4q5_rtN.js} +3 -3
- package/dist/assets/{main--sdaD56A.js.map → main-C4q5_rtN.js.map} +1 -1
- package/dist/assets/{react-vendor-D5Td5IIA.js → react-vendor-pe76PXQl.js} +2 -2
- package/dist/assets/{react-vendor-D5Td5IIA.js.map → react-vendor-pe76PXQl.js.map} +1 -1
- package/dist/index.html +1 -1
- package/package.json +6 -4
- package/src/components/agent-runs/AgentRunTimeline.tsx +700 -0
- package/src/components/agent-sidebar.tsx +18 -2
- package/src/hooks/use-query-hooks.ts +58 -0
- package/src/index.css +24 -0
- package/src/routes/agent-detail.tsx +3 -3
- package/dist/assets/main-DeWqErjb.js +0 -119
- package/dist/assets/main-DeWqErjb.js.map +0 -1
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { useAgentRunDetail, useAgentRuns } from '@/hooks/use-query-hooks';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
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';
|
|
17
|
+
|
|
18
|
+
type AgentRunTimelineProps = {
|
|
19
|
+
agentId: UUID;
|
|
20
|
+
};
|
|
21
|
+
|
|
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
|
|
137
|
+
|
|
138
|
+
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
|
+
const runsQuery = useAgentRuns(agentId);
|
|
143
|
+
const runs = runsQuery.data?.runs ?? [];
|
|
144
|
+
|
|
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
|
+
};
|
|
400
|
+
|
|
401
|
+
const isLoading = runsQuery.isLoading || runDetailQuery.isLoading;
|
|
402
|
+
const errorMessage = runsQuery.error ? (runsQuery.error as Error).message : undefined;
|
|
403
|
+
const hasRuns = runs.length > 0;
|
|
404
|
+
|
|
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 */}
|
|
415
|
+
</div>
|
|
416
|
+
|
|
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>
|
|
572
|
+
</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
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
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>
|
|
661
|
+
|
|
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>
|
|
683
|
+
</div>
|
|
684
|
+
|
|
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
|
+
)}
|
|
696
|
+
</div>
|
|
697
|
+
);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
export default AgentRunTimeline;
|