@townco/debugger 0.1.32 → 0.1.34
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/package.json +14 -14
- package/src/App.tsx +1 -0
- package/src/analysis/analyzer.ts +1 -2
- package/src/analysis/comparison-analyzer.ts +528 -0
- package/src/analysis/comparison-schema.ts +151 -0
- package/src/analysis/comparison-types.ts +194 -0
- package/src/analysis-db.ts +13 -6
- package/src/comparison-db.ts +75 -3
- package/src/components/AnalyzeAllButton.tsx +6 -2
- package/src/components/ComparisonAnalysisDialog.tsx +591 -0
- package/src/components/DebuggerHeader.tsx +0 -1
- package/src/components/LogList.tsx +9 -0
- package/src/components/SessionTraceList.tsx +9 -0
- package/src/components/SpanDetailsPanel.tsx +20 -1
- package/src/components/SpanTimeline.tsx +31 -4
- package/src/components/SpanTree.tsx +10 -1
- package/src/components/TurnMetadataPanel.tsx +0 -1
- package/src/components/UnifiedTimeline.tsx +24 -35
- package/src/components/ui/button.tsx +1 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/checkbox.tsx +1 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/textarea.tsx +1 -1
- package/src/frontend.tsx +2 -0
- package/src/lib/metrics.test.ts +2 -0
- package/src/lib/turnExtractor.ts +28 -0
- package/src/pages/ComparisonView.tsx +584 -92
- package/src/pages/FindSessions.tsx +3 -1
- package/src/pages/TownHall.tsx +30 -14
- package/src/server.ts +177 -7
- package/src/types.ts +4 -0
- package/styles/globals.css +120 -0
- package/tsconfig.json +2 -2
|
@@ -211,7 +211,7 @@ export function SpanDetailsPanel({
|
|
|
211
211
|
const CollapsibleText = ({ text }: { text: string }) => {
|
|
212
212
|
const [expanded, setExpanded] = useState(false);
|
|
213
213
|
const shouldCollapse = text.length > 300;
|
|
214
|
-
const preview = shouldCollapse ? text.slice(0, 300)
|
|
214
|
+
const preview = shouldCollapse ? `${text.slice(0, 300)}...` : text;
|
|
215
215
|
|
|
216
216
|
if (!shouldCollapse) {
|
|
217
217
|
return (
|
|
@@ -247,6 +247,7 @@ export function SpanDetailsPanel({
|
|
|
247
247
|
}: {
|
|
248
248
|
title: string;
|
|
249
249
|
subtitle?: string;
|
|
250
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
250
251
|
content: any;
|
|
251
252
|
borderColor: string;
|
|
252
253
|
bgColor: string;
|
|
@@ -303,11 +304,14 @@ export function SpanDetailsPanel({
|
|
|
303
304
|
result,
|
|
304
305
|
}: {
|
|
305
306
|
toolName: string;
|
|
307
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
306
308
|
input: any;
|
|
309
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
307
310
|
result?: any;
|
|
308
311
|
}) => {
|
|
309
312
|
const [expanded, setExpanded] = useState(false);
|
|
310
313
|
|
|
314
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
311
315
|
const parseContent = (content: any) => {
|
|
312
316
|
if (typeof content === "string") {
|
|
313
317
|
try {
|
|
@@ -319,6 +323,7 @@ export function SpanDetailsPanel({
|
|
|
319
323
|
return content;
|
|
320
324
|
};
|
|
321
325
|
|
|
326
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
322
327
|
const formatContent = (content: any) => {
|
|
323
328
|
const parsed = parseContent(content);
|
|
324
329
|
return typeof parsed === "string"
|
|
@@ -386,6 +391,7 @@ export function SpanDetailsPanel({
|
|
|
386
391
|
|
|
387
392
|
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
388
393
|
|
|
394
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
389
395
|
const parseToolInput = (input: any): any => {
|
|
390
396
|
if (typeof input === "string") {
|
|
391
397
|
try {
|
|
@@ -397,6 +403,7 @@ export function SpanDetailsPanel({
|
|
|
397
403
|
return input;
|
|
398
404
|
};
|
|
399
405
|
|
|
406
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
400
407
|
const renderContent = (content: any) => {
|
|
401
408
|
// Handle string content - parse if it looks like JSON array
|
|
402
409
|
if (typeof content === "string") {
|
|
@@ -418,15 +425,18 @@ export function SpanDetailsPanel({
|
|
|
418
425
|
// Handle array content (can contain text and tool_use blocks)
|
|
419
426
|
if (Array.isArray(content)) {
|
|
420
427
|
// First, group tool_use with their corresponding tool_result
|
|
428
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
421
429
|
const processedBlocks: any[] = [];
|
|
422
430
|
const usedResultIndices = new Set<number>();
|
|
423
431
|
|
|
432
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
424
433
|
content.forEach((block: any, blockIndex: number) => {
|
|
425
434
|
if (block.type === "text") {
|
|
426
435
|
processedBlocks.push({ type: "text", block, index: blockIndex });
|
|
427
436
|
} else if (block.type === "tool_use") {
|
|
428
437
|
// Find the corresponding tool_result
|
|
429
438
|
const resultIndex = content.findIndex(
|
|
439
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
430
440
|
(b: any, idx: number) =>
|
|
431
441
|
b.type === "tool_result" &&
|
|
432
442
|
b.tool_use_id === block.id &&
|
|
@@ -464,6 +474,7 @@ export function SpanDetailsPanel({
|
|
|
464
474
|
|
|
465
475
|
return (
|
|
466
476
|
<div className="flex flex-col gap-2">
|
|
477
|
+
{/* biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content */}
|
|
467
478
|
{processedBlocks.map((item: any) => {
|
|
468
479
|
if (item.type === "text") {
|
|
469
480
|
return (
|
|
@@ -544,9 +555,11 @@ export function SpanDetailsPanel({
|
|
|
544
555
|
<div className="p-3 flex flex-col gap-3">
|
|
545
556
|
{(() => {
|
|
546
557
|
// Group tool_use (from AI messages) with tool results (from tool messages)
|
|
558
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
547
559
|
const processedMessages: any[] = [];
|
|
548
560
|
const usedToolMessageIndices = new Set<number>();
|
|
549
561
|
|
|
562
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
550
563
|
messages.forEach((message: any, msgIndex: number) => {
|
|
551
564
|
const role = message.role || "unknown";
|
|
552
565
|
|
|
@@ -566,7 +579,9 @@ export function SpanDetailsPanel({
|
|
|
566
579
|
|
|
567
580
|
if (Array.isArray(parsedContent)) {
|
|
568
581
|
// Extract tool_use blocks
|
|
582
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
569
583
|
const toolUses: any[] = [];
|
|
584
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
570
585
|
const nonToolBlocks: any[] = [];
|
|
571
586
|
|
|
572
587
|
for (const block of parsedContent) {
|
|
@@ -726,6 +741,7 @@ export function SpanDetailsPanel({
|
|
|
726
741
|
</h2>
|
|
727
742
|
<Dialog.Close asChild>
|
|
728
743
|
<button
|
|
744
|
+
type="button"
|
|
729
745
|
className="size-6 flex items-center justify-center rounded hover:bg-muted transition-colors"
|
|
730
746
|
aria-label="Close"
|
|
731
747
|
>
|
|
@@ -737,6 +753,7 @@ export function SpanDetailsPanel({
|
|
|
737
753
|
{/* Tabs */}
|
|
738
754
|
<div className="flex items-center gap-2">
|
|
739
755
|
<button
|
|
756
|
+
type="button"
|
|
740
757
|
onClick={() => setActiveTab("run")}
|
|
741
758
|
className={cn(
|
|
742
759
|
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
@@ -748,6 +765,7 @@ export function SpanDetailsPanel({
|
|
|
748
765
|
Run
|
|
749
766
|
</button>
|
|
750
767
|
<button
|
|
768
|
+
type="button"
|
|
751
769
|
onClick={() => setActiveTab("logs")}
|
|
752
770
|
className={cn(
|
|
753
771
|
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
@@ -759,6 +777,7 @@ export function SpanDetailsPanel({
|
|
|
759
777
|
Logs
|
|
760
778
|
</button>
|
|
761
779
|
<button
|
|
780
|
+
type="button"
|
|
762
781
|
onClick={() => setActiveTab("feedback")}
|
|
763
782
|
className={cn(
|
|
764
783
|
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
@@ -55,7 +55,7 @@ interface SpanRowProps {
|
|
|
55
55
|
traceEnd: number;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
58
|
+
function _SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
|
|
59
59
|
const [expanded, setExpanded] = useState(false);
|
|
60
60
|
const [showRaw, setShowRaw] = useState(false);
|
|
61
61
|
|
|
@@ -119,10 +119,19 @@ function SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
|
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
121
|
<div>
|
|
122
|
+
{/* biome-ignore lint/a11y/useSemanticElements: complex layout with nested content */}
|
|
122
123
|
<div
|
|
124
|
+
role="button"
|
|
125
|
+
tabIndex={hasDetails ? 0 : undefined}
|
|
123
126
|
className={`py-1 px-2 ${hasDetails ? "cursor-pointer" : ""}`}
|
|
124
127
|
style={{ paddingLeft: span.depth * 20 + 8 }}
|
|
125
128
|
onClick={() => hasDetails && setExpanded(!expanded)}
|
|
129
|
+
onKeyDown={(e) => {
|
|
130
|
+
if (hasDetails && (e.key === "Enter" || e.key === " ")) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
setExpanded(!expanded);
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
126
135
|
>
|
|
127
136
|
<div className="flex items-center hover:bg-muted rounded">
|
|
128
137
|
<SpanIcon type={spanType} className="mr-2" />
|
|
@@ -236,7 +245,7 @@ function SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
|
|
|
236
245
|
)}
|
|
237
246
|
|
|
238
247
|
{span.children.map((child) => (
|
|
239
|
-
<
|
|
248
|
+
<_SpanRow
|
|
240
249
|
key={child.span_id}
|
|
241
250
|
span={child}
|
|
242
251
|
traceStart={traceStart}
|
|
@@ -301,8 +310,8 @@ export function SpanTimeline({
|
|
|
301
310
|
<div className="flex-1 overflow-x-auto">
|
|
302
311
|
{/* Timeline header with time markers */}
|
|
303
312
|
<div className="h-6 px-4 py-1 bg-muted border-b border-border flex items-center text-[10px] text-muted-foreground gap-16">
|
|
304
|
-
{timeMarkers.map((marker
|
|
305
|
-
<span key={
|
|
313
|
+
{timeMarkers.map((marker) => (
|
|
314
|
+
<span key={marker} className="w-[260px]">
|
|
306
315
|
{marker}
|
|
307
316
|
</span>
|
|
308
317
|
))}
|
|
@@ -372,10 +381,19 @@ function RenderSpanTreeRow({
|
|
|
372
381
|
|
|
373
382
|
return (
|
|
374
383
|
<>
|
|
384
|
+
{/* biome-ignore lint/a11y/useSemanticElements: complex layout row */}
|
|
375
385
|
<div
|
|
386
|
+
role="button"
|
|
387
|
+
tabIndex={0}
|
|
376
388
|
className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer transition-colors"
|
|
377
389
|
style={{ paddingLeft: `${span.depth * 20 + 16}px` }}
|
|
378
390
|
onClick={() => onSpanClick(span)}
|
|
391
|
+
onKeyDown={(e) => {
|
|
392
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
onSpanClick(span);
|
|
395
|
+
}
|
|
396
|
+
}}
|
|
379
397
|
>
|
|
380
398
|
<SpanIcon type={spanType} className="shrink-0" />
|
|
381
399
|
<span className="text-sm truncate flex-1">{displayName}</span>
|
|
@@ -422,9 +440,18 @@ function RenderSpanTreeTimeline({
|
|
|
422
440
|
|
|
423
441
|
return (
|
|
424
442
|
<>
|
|
443
|
+
{/* biome-ignore lint/a11y/useSemanticElements: complex timeline bar layout */}
|
|
425
444
|
<div
|
|
445
|
+
role="button"
|
|
446
|
+
tabIndex={0}
|
|
426
447
|
className="h-[38px] border-b border-border relative flex items-center px-4 hover:bg-muted/50 cursor-pointer transition-colors"
|
|
427
448
|
onClick={() => onSpanClick(span)}
|
|
449
|
+
onKeyDown={(e) => {
|
|
450
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
onSpanClick(span);
|
|
453
|
+
}
|
|
454
|
+
}}
|
|
428
455
|
>
|
|
429
456
|
<div
|
|
430
457
|
className={`absolute h-[18px] rounded ${barColor} flex items-center px-1 pointer-events-none`}
|
|
@@ -53,7 +53,7 @@ function SpanRow({
|
|
|
53
53
|
span: SpanNode;
|
|
54
54
|
onSpanClick: (span: SpanNode) => void;
|
|
55
55
|
}) {
|
|
56
|
-
const [expanded,
|
|
56
|
+
const [expanded, _setExpanded] = useState(false);
|
|
57
57
|
const [showRaw, setShowRaw] = useState(false);
|
|
58
58
|
|
|
59
59
|
const attrs = parseAttributes(span.attributes);
|
|
@@ -104,10 +104,19 @@ function SpanRow({
|
|
|
104
104
|
|
|
105
105
|
return (
|
|
106
106
|
<div>
|
|
107
|
+
{/* biome-ignore lint/a11y/useSemanticElements: complex tree layout */}
|
|
107
108
|
<div
|
|
109
|
+
role="button"
|
|
110
|
+
tabIndex={hasDetails ? 0 : undefined}
|
|
108
111
|
className={`flex items-center py-1 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
|
|
109
112
|
style={{ paddingLeft: span.depth * 20 + 8 }}
|
|
110
113
|
onClick={() => onSpanClick(span)}
|
|
114
|
+
onKeyDown={(e) => {
|
|
115
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
onSpanClick(span);
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
111
120
|
>
|
|
112
121
|
<span className={`${statusColor} mr-2`}>●</span>
|
|
113
122
|
<span className="font-medium flex-1 truncate">{displayName}</span>
|
|
@@ -22,20 +22,6 @@ function parseAttributes(attrs: string | null): Record<string, unknown> {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Generate distinct colors for turns
|
|
26
|
-
const TURN_COLORS = [
|
|
27
|
-
"bg-blue-500/10",
|
|
28
|
-
"bg-purple-500/10",
|
|
29
|
-
"bg-green-500/10",
|
|
30
|
-
"bg-orange-500/10",
|
|
31
|
-
"bg-pink-500/10",
|
|
32
|
-
"bg-yellow-500/10",
|
|
33
|
-
"bg-red-500/10",
|
|
34
|
-
"bg-indigo-500/10",
|
|
35
|
-
"bg-teal-500/10",
|
|
36
|
-
"bg-cyan-500/10",
|
|
37
|
-
];
|
|
38
|
-
|
|
39
25
|
export function UnifiedTimeline({
|
|
40
26
|
spans,
|
|
41
27
|
traces,
|
|
@@ -91,16 +77,6 @@ export function UnifiedTimeline({
|
|
|
91
77
|
});
|
|
92
78
|
}, [traces, traceData]);
|
|
93
79
|
|
|
94
|
-
// Helper to find span node in tree
|
|
95
|
-
const findSpanNode = (nodes: SpanNode[], spanId: string): SpanNode | null => {
|
|
96
|
-
for (const node of nodes) {
|
|
97
|
-
if (node.span_id === spanId) return node;
|
|
98
|
-
const found = findSpanNode(node.children, spanId);
|
|
99
|
-
if (found) return found;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
80
|
// Calculate cumulative width for each turn
|
|
105
81
|
const turnLayouts = useMemo(() => {
|
|
106
82
|
let cumulativePercent = 0;
|
|
@@ -130,18 +106,16 @@ export function UnifiedTimeline({
|
|
|
130
106
|
}, [turnBoundaries]);
|
|
131
107
|
|
|
132
108
|
// Calculate global timeline bounds
|
|
133
|
-
const
|
|
109
|
+
const _globalStart = useMemo(() => {
|
|
134
110
|
if (spans.length === 0) return 0;
|
|
135
111
|
return Math.min(...spans.map((s) => s.start_time_unix_nano));
|
|
136
112
|
}, [spans]);
|
|
137
113
|
|
|
138
|
-
const
|
|
114
|
+
const _globalEnd = useMemo(() => {
|
|
139
115
|
if (spans.length === 0) return 0;
|
|
140
116
|
return Math.max(...spans.map((s) => s.end_time_unix_nano));
|
|
141
117
|
}, [spans]);
|
|
142
118
|
|
|
143
|
-
const totalDurationMs = (globalEnd - globalStart) / 1_000_000;
|
|
144
|
-
|
|
145
119
|
// Zoom handler (scroll is now native)
|
|
146
120
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
|
147
121
|
if (e.ctrlKey || e.metaKey) {
|
|
@@ -187,7 +161,7 @@ export function UnifiedTimeline({
|
|
|
187
161
|
});
|
|
188
162
|
|
|
189
163
|
return filtered;
|
|
190
|
-
}, [spanTree, spans, turnLayouts]);
|
|
164
|
+
}, [spanTree, spans, turnLayouts, flattenSpanTree]);
|
|
191
165
|
|
|
192
166
|
if (spans.length === 0) {
|
|
193
167
|
return (
|
|
@@ -203,6 +177,7 @@ export function UnifiedTimeline({
|
|
|
203
177
|
{/* Zoom controls */}
|
|
204
178
|
<div className="flex items-center gap-4 text-sm">
|
|
205
179
|
<button
|
|
180
|
+
type="button"
|
|
206
181
|
onClick={() => setZoom((prev) => Math.max(1, prev * 0.8))}
|
|
207
182
|
className="px-3 py-1 border border-border rounded hover:bg-muted"
|
|
208
183
|
>
|
|
@@ -212,12 +187,14 @@ export function UnifiedTimeline({
|
|
|
212
187
|
{(zoom * 100).toFixed(0)}%
|
|
213
188
|
</span>
|
|
214
189
|
<button
|
|
190
|
+
type="button"
|
|
215
191
|
onClick={() => setZoom((prev) => Math.min(10, prev * 1.2))}
|
|
216
192
|
className="px-3 py-1 border border-border rounded hover:bg-muted"
|
|
217
193
|
>
|
|
218
194
|
Zoom In
|
|
219
195
|
</button>
|
|
220
196
|
<button
|
|
197
|
+
type="button"
|
|
221
198
|
onClick={() => setZoom(1)}
|
|
222
199
|
className="px-3 py-1 border border-border rounded hover:bg-muted"
|
|
223
200
|
>
|
|
@@ -290,8 +267,11 @@ export function UnifiedTimeline({
|
|
|
290
267
|
}
|
|
291
268
|
|
|
292
269
|
return (
|
|
270
|
+
// biome-ignore lint/a11y/useSemanticElements: complex timeline layout
|
|
293
271
|
<div
|
|
294
272
|
key={span.span_id}
|
|
273
|
+
role="button"
|
|
274
|
+
tabIndex={0}
|
|
295
275
|
className={`h-[38px] px-3 py-2 border-b border-border flex items-center gap-2 cursor-pointer transition-colors ${
|
|
296
276
|
hoveredSpanId === span.span_id ? "bg-muted" : ""
|
|
297
277
|
}`}
|
|
@@ -299,6 +279,12 @@ export function UnifiedTimeline({
|
|
|
299
279
|
onMouseEnter={() => setHoveredSpanId(span.span_id)}
|
|
300
280
|
onMouseLeave={() => setHoveredSpanId(null)}
|
|
301
281
|
onClick={() => setSelectedSpan(node)}
|
|
282
|
+
onKeyDown={(e) => {
|
|
283
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
setSelectedSpan(node);
|
|
286
|
+
}
|
|
287
|
+
}}
|
|
302
288
|
>
|
|
303
289
|
<SpanIcon type={spanType} className="w-4 h-4 shrink-0" />
|
|
304
290
|
<span className="text-xs truncate">{displayName}</span>
|
|
@@ -338,11 +324,6 @@ export function UnifiedTimeline({
|
|
|
338
324
|
left: turn.leftPercent * zoom,
|
|
339
325
|
};
|
|
340
326
|
|
|
341
|
-
// Agent message positioned at end of turn (100% of turn width)
|
|
342
|
-
const agentPos = {
|
|
343
|
-
left: (turn.leftPercent + turn.widthPercent) * zoom,
|
|
344
|
-
};
|
|
345
|
-
|
|
346
327
|
return (
|
|
347
328
|
<div key={trace.trace_id}>
|
|
348
329
|
{/* User message bubble - positioned at top, aligned to start of turn */}
|
|
@@ -568,7 +549,6 @@ export function UnifiedTimeline({
|
|
|
568
549
|
);
|
|
569
550
|
}
|
|
570
551
|
|
|
571
|
-
const attrs = parseAttributes(span.attributes);
|
|
572
552
|
const spanType = detectSpanType(span);
|
|
573
553
|
|
|
574
554
|
// Find which turn this span belongs to
|
|
@@ -610,8 +590,11 @@ export function UnifiedTimeline({
|
|
|
610
590
|
1_000_000;
|
|
611
591
|
|
|
612
592
|
return (
|
|
593
|
+
// biome-ignore lint/a11y/useSemanticElements: positioned timeline bar
|
|
613
594
|
<div
|
|
614
595
|
key={span.span_id}
|
|
596
|
+
role="button"
|
|
597
|
+
tabIndex={0}
|
|
615
598
|
className={`absolute h-6 ${barColor} rounded cursor-pointer transition-all flex items-center justify-start px-2 ${
|
|
616
599
|
hoveredSpanId === span.span_id ? "brightness-110" : ""
|
|
617
600
|
}`}
|
|
@@ -625,6 +608,12 @@ export function UnifiedTimeline({
|
|
|
625
608
|
onClick={() => {
|
|
626
609
|
setSelectedSpan(node);
|
|
627
610
|
}}
|
|
611
|
+
onKeyDown={(e) => {
|
|
612
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
613
|
+
e.preventDefault();
|
|
614
|
+
setSelectedSpan(node);
|
|
615
|
+
}
|
|
616
|
+
}}
|
|
628
617
|
>
|
|
629
618
|
<span className="text-[11px] text-white font-medium whitespace-nowrap">
|
|
630
619
|
{durationMs.toFixed(2)}ms
|
|
@@ -14,6 +14,7 @@ interface CheckboxProps {
|
|
|
14
14
|
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
15
15
|
({ id, checked = false, onCheckedChange, disabled, className }, ref) => {
|
|
16
16
|
return (
|
|
17
|
+
// biome-ignore lint/a11y/useSemanticElements: custom checkbox component using button for styling control
|
|
17
18
|
<button
|
|
18
19
|
ref={ref}
|
|
19
20
|
type="button"
|
package/src/frontend.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { StrictMode } from "react";
|
|
|
9
9
|
import { createRoot } from "react-dom/client";
|
|
10
10
|
import { App } from "./App";
|
|
11
11
|
|
|
12
|
+
// biome-ignore lint/style/noNonNullAssertion: root element always exists
|
|
12
13
|
const elem = document.getElementById("root")!;
|
|
13
14
|
const app = (
|
|
14
15
|
<StrictMode>
|
|
@@ -18,6 +19,7 @@ const app = (
|
|
|
18
19
|
|
|
19
20
|
if (import.meta.hot) {
|
|
20
21
|
// With hot module reloading, `import.meta.hot.data` is persisted.
|
|
22
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: HMR pattern
|
|
21
23
|
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
|
22
24
|
root.render(app);
|
|
23
25
|
} else {
|
package/src/lib/metrics.test.ts
CHANGED
|
@@ -29,10 +29,12 @@ describe("metrics", () => {
|
|
|
29
29
|
"gen_ai.usage.input_tokens": 100,
|
|
30
30
|
"gen_ai.usage.output_tokens": 50,
|
|
31
31
|
}),
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: test mock
|
|
32
33
|
} as any,
|
|
33
34
|
{
|
|
34
35
|
name: "tool_call:search",
|
|
35
36
|
attributes: "{}",
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: test mock
|
|
36
38
|
} as any,
|
|
37
39
|
];
|
|
38
40
|
|
package/src/lib/turnExtractor.ts
CHANGED
|
@@ -117,12 +117,40 @@ export function extractTurnMessages(spans: Span[], logs?: Log[]): TurnMessages {
|
|
|
117
117
|
} else if (event.type === "tool_call") {
|
|
118
118
|
const toolName = attrs["tool.name"] as string;
|
|
119
119
|
if (toolName) {
|
|
120
|
+
// Parse tool input and output
|
|
121
|
+
let toolInput: unknown = null;
|
|
122
|
+
let toolOutput: unknown = null;
|
|
123
|
+
|
|
124
|
+
const rawInput = attrs["tool.input"];
|
|
125
|
+
if (rawInput) {
|
|
126
|
+
try {
|
|
127
|
+
toolInput =
|
|
128
|
+
typeof rawInput === "string" ? JSON.parse(rawInput) : rawInput;
|
|
129
|
+
} catch {
|
|
130
|
+
toolInput = rawInput;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const rawOutput = attrs["tool.output"];
|
|
135
|
+
if (rawOutput) {
|
|
136
|
+
try {
|
|
137
|
+
toolOutput =
|
|
138
|
+
typeof rawOutput === "string"
|
|
139
|
+
? JSON.parse(rawOutput)
|
|
140
|
+
: rawOutput;
|
|
141
|
+
} catch {
|
|
142
|
+
toolOutput = rawOutput;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
result.agentMessages.push({
|
|
121
147
|
content: toolName,
|
|
122
148
|
spanId: event.span.span_id,
|
|
123
149
|
timestamp: event.span.end_time_unix_nano,
|
|
124
150
|
type: "tool_call",
|
|
125
151
|
toolName,
|
|
152
|
+
toolInput,
|
|
153
|
+
toolOutput,
|
|
126
154
|
});
|
|
127
155
|
}
|
|
128
156
|
}
|