@townco/debugger 0.1.66 → 0.1.68
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 +3 -3
- package/src/App.tsx +9 -2
- package/src/components/ConversationPanel.tsx +392 -0
- package/src/components/DebuggerHeader.tsx +1 -1
- package/src/components/DiagnosticDetailsPanel.tsx +1095 -0
- package/src/components/DiagnosticPanel.tsx +111 -0
- package/src/components/NavBar.tsx +130 -0
- package/src/components/SpanDetailsPanel.tsx +2 -2
- package/src/components/SpanIcon.tsx +75 -24
- package/src/components/SpanTimeline.tsx +2 -3
- package/src/components/SpansLogsList.tsx +477 -0
- package/src/components/UnifiedTimeline.tsx +1 -2
- package/src/components/ViewControlBar.tsx +79 -0
- package/src/lib/segmentation.ts +391 -0
- package/src/lib/toolRegistry.ts +424 -0
- package/src/pages/SessionLogsView.tsx +585 -0
- package/src/pages/SessionView.tsx +24 -13
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { detectSpanType } from "../lib/spanTypeDetector";
|
|
5
|
+
import type { Log, SpanNode } from "../types";
|
|
6
|
+
import { SpanIcon } from "./SpanIcon";
|
|
7
|
+
|
|
8
|
+
interface DiagnosticDetailsPanelProps {
|
|
9
|
+
span: SpanNode | null;
|
|
10
|
+
log: Log | null;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
allSpans: SpanNode[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseAttributes(attrs: string | null): Record<string, unknown> {
|
|
16
|
+
if (!attrs) return {};
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(attrs);
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getDisplayName(span: SpanNode): string {
|
|
25
|
+
const attrs = parseAttributes(span.attributes);
|
|
26
|
+
const spanType = detectSpanType(span);
|
|
27
|
+
|
|
28
|
+
if (spanType === "tool_call") {
|
|
29
|
+
const toolName = attrs["tool.name"] as string;
|
|
30
|
+
if (toolName !== "Task") return toolName || span.name;
|
|
31
|
+
|
|
32
|
+
// Parse tool.input to extract agentName for subagent spans
|
|
33
|
+
try {
|
|
34
|
+
const toolInput = attrs["tool.input"];
|
|
35
|
+
const input =
|
|
36
|
+
typeof toolInput === "string" ? JSON.parse(toolInput) : toolInput;
|
|
37
|
+
if (input?.agentName) {
|
|
38
|
+
return `Subagent (${input.agentName})`;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Fall back to "Task" if parsing fails
|
|
42
|
+
}
|
|
43
|
+
return toolName || span.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (spanType === "chat") {
|
|
47
|
+
return (attrs["gen_ai.request.model"] as string) || span.name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return span.name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findParentSpan(span: SpanNode, allSpans: SpanNode[]): SpanNode | null {
|
|
54
|
+
if (!span.parent_span_id) return null;
|
|
55
|
+
|
|
56
|
+
const findSpan = (spans: SpanNode[]): SpanNode | null => {
|
|
57
|
+
for (const s of spans) {
|
|
58
|
+
if (s.span_id === span.parent_span_id) return s;
|
|
59
|
+
const found = findSpan(s.children);
|
|
60
|
+
if (found) return found;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return findSpan(allSpans);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatDuration(durationMs: number): string {
|
|
69
|
+
if (durationMs < 1) {
|
|
70
|
+
return `${(durationMs * 1000).toFixed(2)}μs`;
|
|
71
|
+
}
|
|
72
|
+
if (durationMs < 1000) {
|
|
73
|
+
return `${durationMs.toFixed(2)}ms`;
|
|
74
|
+
}
|
|
75
|
+
return `${(durationMs / 1000).toFixed(2)}s`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatTimestamp(timestampNano: number): string {
|
|
79
|
+
const date = new Date(timestampNano / 1_000_000);
|
|
80
|
+
return date.toLocaleString("en-US", {
|
|
81
|
+
month: "numeric",
|
|
82
|
+
day: "numeric",
|
|
83
|
+
year: "numeric",
|
|
84
|
+
hour: "numeric",
|
|
85
|
+
minute: "numeric",
|
|
86
|
+
second: "numeric",
|
|
87
|
+
hour12: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function countToolUsage(span: SpanNode): {
|
|
92
|
+
toolCount: number;
|
|
93
|
+
callCount: number;
|
|
94
|
+
} {
|
|
95
|
+
const uniqueTools = new Set<string>();
|
|
96
|
+
let totalCalls = 0;
|
|
97
|
+
|
|
98
|
+
const traverse = (node: SpanNode) => {
|
|
99
|
+
const attrs = parseAttributes(node.attributes);
|
|
100
|
+
if (node.name === "agent.tool_call") {
|
|
101
|
+
const toolName = attrs["tool.name"] as string;
|
|
102
|
+
if (toolName) {
|
|
103
|
+
uniqueTools.add(toolName);
|
|
104
|
+
totalCalls++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const child of node.children) {
|
|
108
|
+
traverse(child);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
traverse(span);
|
|
113
|
+
return { toolCount: uniqueTools.size, callCount: totalCalls };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function countLogs(span: SpanNode): number {
|
|
117
|
+
let count = 0;
|
|
118
|
+
const traverse = (node: SpanNode) => {
|
|
119
|
+
if (node.events) {
|
|
120
|
+
try {
|
|
121
|
+
const events = JSON.parse(node.events);
|
|
122
|
+
count += Array.isArray(events) ? events.length : 0;
|
|
123
|
+
} catch {
|
|
124
|
+
// Ignore parse errors
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const child of node.children) {
|
|
128
|
+
traverse(child);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
traverse(span);
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function countTokens(span: SpanNode): number {
|
|
136
|
+
let tokens = 0;
|
|
137
|
+
const traverse = (node: SpanNode) => {
|
|
138
|
+
const attrs = parseAttributes(node.attributes);
|
|
139
|
+
const inputTokens = attrs["gen_ai.usage.input_tokens"] as number;
|
|
140
|
+
const outputTokens = attrs["gen_ai.usage.output_tokens"] as number;
|
|
141
|
+
if (inputTokens) tokens += inputTokens;
|
|
142
|
+
if (outputTokens) tokens += outputTokens;
|
|
143
|
+
for (const child of node.children) {
|
|
144
|
+
traverse(child);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
traverse(span);
|
|
148
|
+
return tokens;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const JsonSection = ({ title, data }: { title: string; data: unknown }) => {
|
|
152
|
+
if (!data) return null;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const jsonString = typeof data === "string" ? data : JSON.stringify(data);
|
|
156
|
+
const parsed = JSON.parse(jsonString);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
160
|
+
<div className="p-2 border-b border-border">
|
|
161
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
162
|
+
{title}
|
|
163
|
+
</h3>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="p-3">
|
|
166
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
167
|
+
{JSON.stringify(parsed, null, 2)}
|
|
168
|
+
</pre>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
} catch {
|
|
173
|
+
// If parse fails, show raw data
|
|
174
|
+
return (
|
|
175
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
176
|
+
<div className="p-2 border-b border-border">
|
|
177
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
178
|
+
{title}
|
|
179
|
+
</h3>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="p-3">
|
|
182
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
183
|
+
{String(data)}
|
|
184
|
+
</pre>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const CollapsibleText = ({ text }: { text: string }) => {
|
|
192
|
+
const [expanded, setExpanded] = useState(false);
|
|
193
|
+
const shouldCollapse = text.length > 300;
|
|
194
|
+
const preview = shouldCollapse ? `${text.slice(0, 300)}...` : text;
|
|
195
|
+
|
|
196
|
+
if (!shouldCollapse) {
|
|
197
|
+
return (
|
|
198
|
+
<div className="text-xs text-foreground whitespace-pre-wrap break-words">
|
|
199
|
+
{text}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<div className="text-xs text-foreground whitespace-pre-wrap break-words">
|
|
207
|
+
{expanded ? text : preview}
|
|
208
|
+
</div>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={() => setExpanded(!expanded)}
|
|
212
|
+
className="text-[10px] text-purple-600 hover:text-purple-700 mt-1 font-medium"
|
|
213
|
+
>
|
|
214
|
+
{expanded ? "Show less" : "Show more"}
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const CollapsibleToolBlock = ({
|
|
221
|
+
title,
|
|
222
|
+
subtitle,
|
|
223
|
+
content,
|
|
224
|
+
borderColor,
|
|
225
|
+
bgColor,
|
|
226
|
+
badgeColor,
|
|
227
|
+
}: {
|
|
228
|
+
title: string;
|
|
229
|
+
subtitle?: string;
|
|
230
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
231
|
+
content: any;
|
|
232
|
+
borderColor: string;
|
|
233
|
+
bgColor: string;
|
|
234
|
+
badgeColor: string;
|
|
235
|
+
}) => {
|
|
236
|
+
const [expanded, setExpanded] = useState(false);
|
|
237
|
+
|
|
238
|
+
// Parse content as JSON if it's a string
|
|
239
|
+
let displayContent = content;
|
|
240
|
+
if (typeof content === "string") {
|
|
241
|
+
try {
|
|
242
|
+
displayContent = JSON.parse(content);
|
|
243
|
+
} catch {
|
|
244
|
+
// Keep as string if parsing fails
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const contentString =
|
|
249
|
+
typeof displayContent === "string"
|
|
250
|
+
? displayContent
|
|
251
|
+
: JSON.stringify(displayContent, null, 2);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className={`border-l-2 ${borderColor} pl-2 ${bgColor} rounded py-1`}>
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={() => setExpanded(!expanded)}
|
|
258
|
+
className="flex items-center gap-2 mb-1 w-full hover:opacity-80 transition-opacity"
|
|
259
|
+
>
|
|
260
|
+
<span className={`text-[9px] font-semibold ${badgeColor} uppercase`}>
|
|
261
|
+
{title}
|
|
262
|
+
</span>
|
|
263
|
+
{subtitle && (
|
|
264
|
+
<span className="text-[10px] text-foreground font-mono">
|
|
265
|
+
{subtitle}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
<span className="text-[10px] text-muted-foreground ml-auto">
|
|
269
|
+
{expanded ? "▼" : "▶"}
|
|
270
|
+
</span>
|
|
271
|
+
</button>
|
|
272
|
+
{expanded && (
|
|
273
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all mt-1">
|
|
274
|
+
{contentString}
|
|
275
|
+
</pre>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const ToolCallWithResult = ({
|
|
282
|
+
toolName,
|
|
283
|
+
input,
|
|
284
|
+
result,
|
|
285
|
+
}: {
|
|
286
|
+
toolName: string;
|
|
287
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
288
|
+
input: any;
|
|
289
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
290
|
+
result?: any;
|
|
291
|
+
}) => {
|
|
292
|
+
const [expanded, setExpanded] = useState(false);
|
|
293
|
+
|
|
294
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
295
|
+
const parseContent = (content: any) => {
|
|
296
|
+
if (typeof content === "string") {
|
|
297
|
+
try {
|
|
298
|
+
return JSON.parse(content);
|
|
299
|
+
} catch {
|
|
300
|
+
return content;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return content;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
307
|
+
const formatContent = (content: any) => {
|
|
308
|
+
const parsed = parseContent(content);
|
|
309
|
+
return typeof parsed === "string"
|
|
310
|
+
? parsed
|
|
311
|
+
: JSON.stringify(parsed, null, 2);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="border-l-2 border-blue-500 pl-2 bg-blue-500/5 rounded py-1">
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
onClick={() => setExpanded(!expanded)}
|
|
319
|
+
className="flex items-center gap-2 mb-1 w-full hover:opacity-80 transition-opacity"
|
|
320
|
+
>
|
|
321
|
+
<span className="text-[9px] font-semibold text-blue-600 uppercase">
|
|
322
|
+
Tool Call
|
|
323
|
+
</span>
|
|
324
|
+
<span className="text-[10px] text-foreground font-mono">
|
|
325
|
+
{toolName}
|
|
326
|
+
</span>
|
|
327
|
+
<span className="text-[10px] text-muted-foreground ml-auto">
|
|
328
|
+
{expanded ? "▼" : "▶"}
|
|
329
|
+
</span>
|
|
330
|
+
</button>
|
|
331
|
+
{expanded && (
|
|
332
|
+
<div className="flex flex-col gap-2 mt-1">
|
|
333
|
+
{/* Input section */}
|
|
334
|
+
<div>
|
|
335
|
+
<div className="text-[9px] font-semibold text-purple-600 uppercase mb-1">
|
|
336
|
+
Input
|
|
337
|
+
</div>
|
|
338
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all bg-background/50 rounded p-2">
|
|
339
|
+
{formatContent(input)}
|
|
340
|
+
</pre>
|
|
341
|
+
</div>
|
|
342
|
+
{/* Result section */}
|
|
343
|
+
{result !== undefined && (
|
|
344
|
+
<div>
|
|
345
|
+
<div className="text-[9px] font-semibold text-green-600 uppercase mb-1">
|
|
346
|
+
Result
|
|
347
|
+
</div>
|
|
348
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all bg-background/50 rounded p-2">
|
|
349
|
+
{formatContent(result)}
|
|
350
|
+
</pre>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const MessageSection = ({ title, data }: { title: string; data: unknown }) => {
|
|
360
|
+
if (!data) return null;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const jsonString = typeof data === "string" ? data : JSON.stringify(data);
|
|
364
|
+
const messages = JSON.parse(jsonString);
|
|
365
|
+
|
|
366
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
367
|
+
|
|
368
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
369
|
+
const parseToolInput = (input: any): any => {
|
|
370
|
+
if (typeof input === "string") {
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(input);
|
|
373
|
+
} catch {
|
|
374
|
+
return input;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return input;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
381
|
+
const renderContent = (content: any) => {
|
|
382
|
+
// Handle string content - parse if it looks like JSON array
|
|
383
|
+
if (typeof content === "string") {
|
|
384
|
+
// Try to parse if it looks like a JSON array
|
|
385
|
+
if (content.trim().startsWith("[")) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(content);
|
|
388
|
+
if (Array.isArray(parsed)) {
|
|
389
|
+
return renderContent(parsed);
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Not valid JSON, fall through to text display
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return <CollapsibleText text={content} />;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Handle array content (can contain text and tool_use blocks)
|
|
400
|
+
if (Array.isArray(content)) {
|
|
401
|
+
// First, group tool_use with their corresponding tool_result
|
|
402
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
403
|
+
const processedBlocks: any[] = [];
|
|
404
|
+
const usedResultIndices = new Set<number>();
|
|
405
|
+
|
|
406
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
407
|
+
content.forEach((block: any, blockIndex: number) => {
|
|
408
|
+
if (block.type === "text") {
|
|
409
|
+
processedBlocks.push({ type: "text", block, index: blockIndex });
|
|
410
|
+
} else if (block.type === "tool_use") {
|
|
411
|
+
// Find the corresponding tool_result
|
|
412
|
+
const resultIndex = content.findIndex(
|
|
413
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
414
|
+
(b: any, idx: number) =>
|
|
415
|
+
b.type === "tool_result" &&
|
|
416
|
+
b.tool_use_id === block.id &&
|
|
417
|
+
!usedResultIndices.has(idx),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (resultIndex !== -1) {
|
|
421
|
+
usedResultIndices.add(resultIndex);
|
|
422
|
+
processedBlocks.push({
|
|
423
|
+
type: "tool_call_with_result",
|
|
424
|
+
toolUse: block,
|
|
425
|
+
toolResult: content[resultIndex],
|
|
426
|
+
index: blockIndex,
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
// No result found, show just the tool_use
|
|
430
|
+
processedBlocks.push({
|
|
431
|
+
type: "tool_use",
|
|
432
|
+
block,
|
|
433
|
+
index: blockIndex,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
} else if (
|
|
437
|
+
block.type === "tool_result" &&
|
|
438
|
+
!usedResultIndices.has(blockIndex)
|
|
439
|
+
) {
|
|
440
|
+
// Orphaned tool_result (no matching tool_use)
|
|
441
|
+
processedBlocks.push({
|
|
442
|
+
type: "tool_result",
|
|
443
|
+
block,
|
|
444
|
+
index: blockIndex,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div className="flex flex-col gap-2">
|
|
451
|
+
{/* biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content */}
|
|
452
|
+
{processedBlocks.map((item: any) => {
|
|
453
|
+
if (item.type === "text") {
|
|
454
|
+
return (
|
|
455
|
+
<CollapsibleText key={item.index} text={item.block.text} />
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (item.type === "tool_call_with_result") {
|
|
460
|
+
const parsedInput = parseToolInput(item.toolUse.input);
|
|
461
|
+
return (
|
|
462
|
+
<ToolCallWithResult
|
|
463
|
+
key={item.index}
|
|
464
|
+
toolName={item.toolUse.name}
|
|
465
|
+
input={parsedInput}
|
|
466
|
+
result={item.toolResult.content}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (item.type === "tool_use") {
|
|
472
|
+
const parsedInput = parseToolInput(item.block.input);
|
|
473
|
+
return (
|
|
474
|
+
<CollapsibleToolBlock
|
|
475
|
+
key={item.index}
|
|
476
|
+
title="Tool Use"
|
|
477
|
+
subtitle={item.block.name}
|
|
478
|
+
content={parsedInput}
|
|
479
|
+
borderColor="border-purple-500"
|
|
480
|
+
bgColor="bg-purple-500/5"
|
|
481
|
+
badgeColor="text-purple-600"
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (item.type === "tool_result") {
|
|
487
|
+
return (
|
|
488
|
+
<CollapsibleToolBlock
|
|
489
|
+
key={item.index}
|
|
490
|
+
title="Tool Result"
|
|
491
|
+
subtitle=""
|
|
492
|
+
content={item.block.content}
|
|
493
|
+
borderColor="border-green-500"
|
|
494
|
+
bgColor="bg-green-500/5"
|
|
495
|
+
badgeColor="text-green-600"
|
|
496
|
+
/>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Unknown block type - show as JSON
|
|
501
|
+
return (
|
|
502
|
+
<pre
|
|
503
|
+
key={item.index}
|
|
504
|
+
className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all"
|
|
505
|
+
>
|
|
506
|
+
{JSON.stringify(item.block, null, 2)}
|
|
507
|
+
</pre>
|
|
508
|
+
);
|
|
509
|
+
})}
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Fallback for other content types
|
|
515
|
+
return (
|
|
516
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all">
|
|
517
|
+
{JSON.stringify(content, null, 2)}
|
|
518
|
+
</pre>
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
524
|
+
<div className="p-2 border-b border-border">
|
|
525
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
526
|
+
{title}
|
|
527
|
+
</h3>
|
|
528
|
+
</div>
|
|
529
|
+
<div className="p-3 flex flex-col gap-3">
|
|
530
|
+
{(() => {
|
|
531
|
+
// Group tool_use (from AI messages) with tool results (from tool messages)
|
|
532
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
533
|
+
const processedMessages: any[] = [];
|
|
534
|
+
const usedToolMessageIndices = new Set<number>();
|
|
535
|
+
|
|
536
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
537
|
+
messages.forEach((message: any, msgIndex: number) => {
|
|
538
|
+
const role = message.role || "unknown";
|
|
539
|
+
|
|
540
|
+
// Handle AI messages - check for tool_use blocks
|
|
541
|
+
if (role === "ai") {
|
|
542
|
+
const content = message.content;
|
|
543
|
+
let parsedContent = content;
|
|
544
|
+
|
|
545
|
+
if (
|
|
546
|
+
typeof content === "string" &&
|
|
547
|
+
content.trim().startsWith("[")
|
|
548
|
+
) {
|
|
549
|
+
try {
|
|
550
|
+
parsedContent = JSON.parse(content);
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (Array.isArray(parsedContent)) {
|
|
555
|
+
// Extract tool_use blocks
|
|
556
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
557
|
+
const toolUses: any[] = [];
|
|
558
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
|
|
559
|
+
const nonToolBlocks: any[] = [];
|
|
560
|
+
|
|
561
|
+
for (const block of parsedContent) {
|
|
562
|
+
if (block.type === "tool_use") {
|
|
563
|
+
toolUses.push(block);
|
|
564
|
+
} else {
|
|
565
|
+
nonToolBlocks.push(block);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Add non-tool content if any
|
|
570
|
+
if (nonToolBlocks.length > 0) {
|
|
571
|
+
processedMessages.push({
|
|
572
|
+
type: "content",
|
|
573
|
+
role,
|
|
574
|
+
content: nonToolBlocks,
|
|
575
|
+
index: msgIndex,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// For each tool_use, find the matching tool message
|
|
580
|
+
for (const toolUse of toolUses) {
|
|
581
|
+
// Find next unused tool message
|
|
582
|
+
let foundToolMessage = false;
|
|
583
|
+
for (let i = msgIndex + 1; i < messages.length; i++) {
|
|
584
|
+
if (
|
|
585
|
+
messages[i].role === "tool" &&
|
|
586
|
+
!usedToolMessageIndices.has(i)
|
|
587
|
+
) {
|
|
588
|
+
usedToolMessageIndices.add(i);
|
|
589
|
+
processedMessages.push({
|
|
590
|
+
type: "tool_call_with_result",
|
|
591
|
+
toolUse: toolUse,
|
|
592
|
+
toolResult: messages[i].content,
|
|
593
|
+
index: msgIndex,
|
|
594
|
+
});
|
|
595
|
+
foundToolMessage = true;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// If no tool message found, show just the tool_use
|
|
601
|
+
if (!foundToolMessage) {
|
|
602
|
+
processedMessages.push({
|
|
603
|
+
type: "tool_use_only",
|
|
604
|
+
toolUse: toolUse,
|
|
605
|
+
index: msgIndex,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
// Non-array content, render normally
|
|
611
|
+
processedMessages.push({
|
|
612
|
+
type: "content",
|
|
613
|
+
role,
|
|
614
|
+
content: parsedContent,
|
|
615
|
+
index: msgIndex,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
} else if (role === "tool") {
|
|
619
|
+
// Skip if already matched, otherwise show as orphan result
|
|
620
|
+
if (!usedToolMessageIndices.has(msgIndex)) {
|
|
621
|
+
processedMessages.push({
|
|
622
|
+
type: "tool_result_only",
|
|
623
|
+
content: message.content,
|
|
624
|
+
index: msgIndex,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
// Other roles (system, human, user, etc.)
|
|
629
|
+
processedMessages.push({
|
|
630
|
+
type: "content",
|
|
631
|
+
role,
|
|
632
|
+
content: message.content,
|
|
633
|
+
index: msgIndex,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Render processed messages
|
|
639
|
+
return processedMessages.map((item, idx) => {
|
|
640
|
+
if (item.type === "tool_call_with_result") {
|
|
641
|
+
return (
|
|
642
|
+
<ToolCallWithResult
|
|
643
|
+
key={`${item.index}-${idx}`}
|
|
644
|
+
toolName={item.toolUse.name}
|
|
645
|
+
input={parseToolInput(item.toolUse.input)}
|
|
646
|
+
result={item.toolResult}
|
|
647
|
+
/>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (item.type === "tool_use_only") {
|
|
652
|
+
return (
|
|
653
|
+
<CollapsibleToolBlock
|
|
654
|
+
key={`${item.index}-${idx}`}
|
|
655
|
+
title="Tool Use"
|
|
656
|
+
subtitle={item.toolUse.name}
|
|
657
|
+
content={parseToolInput(item.toolUse.input)}
|
|
658
|
+
borderColor="border-purple-500"
|
|
659
|
+
bgColor="bg-purple-500/5"
|
|
660
|
+
badgeColor="text-purple-600"
|
|
661
|
+
/>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (item.type === "tool_result_only") {
|
|
666
|
+
return (
|
|
667
|
+
<CollapsibleToolBlock
|
|
668
|
+
key={`${item.index}-${idx}`}
|
|
669
|
+
title="Tool Result"
|
|
670
|
+
subtitle=""
|
|
671
|
+
content={item.content}
|
|
672
|
+
borderColor="border-green-500"
|
|
673
|
+
bgColor="bg-green-500/5"
|
|
674
|
+
badgeColor="text-green-600"
|
|
675
|
+
/>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Regular content
|
|
680
|
+
return (
|
|
681
|
+
<div
|
|
682
|
+
key={`${item.index}-${idx}`}
|
|
683
|
+
className="border border-border rounded p-2 bg-background"
|
|
684
|
+
>
|
|
685
|
+
<div className="flex items-center gap-2 mb-2">
|
|
686
|
+
<span className="text-[10px] font-semibold text-muted-foreground uppercase">
|
|
687
|
+
{item.role}
|
|
688
|
+
</span>
|
|
689
|
+
</div>
|
|
690
|
+
{renderContent(item.content)}
|
|
691
|
+
</div>
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
})()}
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
);
|
|
698
|
+
} catch {
|
|
699
|
+
// If parse fails, fall back to JSON display
|
|
700
|
+
return <JsonSection title={title} data={data} />;
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
function SpanDetails({
|
|
705
|
+
span,
|
|
706
|
+
allSpans,
|
|
707
|
+
}: {
|
|
708
|
+
span: SpanNode;
|
|
709
|
+
allSpans: SpanNode[];
|
|
710
|
+
}) {
|
|
711
|
+
const [activeTab, setActiveTab] = useState<"run" | "logs" | "feedback">(
|
|
712
|
+
"run",
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const spanType = detectSpanType(span);
|
|
716
|
+
const parentSpan = findParentSpan(span, allSpans);
|
|
717
|
+
const attrs = parseAttributes(span.attributes);
|
|
718
|
+
const resourceAttrs = parseAttributes(span.resource_attributes);
|
|
719
|
+
const { toolCount, callCount } = countToolUsage(span);
|
|
720
|
+
const logCount = countLogs(span);
|
|
721
|
+
const tokenCount = countTokens(span);
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<div className="flex flex-col gap-4">
|
|
725
|
+
{/* Tabs */}
|
|
726
|
+
<div className="flex items-center gap-2">
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
onClick={() => setActiveTab("run")}
|
|
730
|
+
className={cn(
|
|
731
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
732
|
+
activeTab === "run"
|
|
733
|
+
? "bg-muted text-foreground"
|
|
734
|
+
: "text-muted-foreground hover:text-foreground",
|
|
735
|
+
)}
|
|
736
|
+
>
|
|
737
|
+
Run
|
|
738
|
+
</button>
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
onClick={() => setActiveTab("logs")}
|
|
742
|
+
className={cn(
|
|
743
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
744
|
+
activeTab === "logs"
|
|
745
|
+
? "bg-muted text-foreground"
|
|
746
|
+
: "text-muted-foreground hover:text-foreground",
|
|
747
|
+
)}
|
|
748
|
+
>
|
|
749
|
+
Logs
|
|
750
|
+
</button>
|
|
751
|
+
<button
|
|
752
|
+
type="button"
|
|
753
|
+
onClick={() => setActiveTab("feedback")}
|
|
754
|
+
className={cn(
|
|
755
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
756
|
+
activeTab === "feedback"
|
|
757
|
+
? "bg-muted text-foreground"
|
|
758
|
+
: "text-muted-foreground hover:text-foreground",
|
|
759
|
+
)}
|
|
760
|
+
>
|
|
761
|
+
Feedback
|
|
762
|
+
</button>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
{/* Content */}
|
|
766
|
+
{activeTab === "run" && (
|
|
767
|
+
<div className="flex flex-col gap-4">
|
|
768
|
+
{/* Details section */}
|
|
769
|
+
<div className="flex flex-col">
|
|
770
|
+
{/* Parent Span */}
|
|
771
|
+
{parentSpan && (
|
|
772
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
773
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
774
|
+
Parent Span
|
|
775
|
+
</span>
|
|
776
|
+
<div className="flex items-center gap-2 flex-1 overflow-hidden">
|
|
777
|
+
<SpanIcon span={parentSpan} className="size-5 shrink-0" />
|
|
778
|
+
<span className="text-sm text-foreground truncate">
|
|
779
|
+
{getDisplayName(parentSpan)}
|
|
780
|
+
</span>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
)}
|
|
784
|
+
|
|
785
|
+
{/* Span ID */}
|
|
786
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
787
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
788
|
+
Span ID
|
|
789
|
+
</span>
|
|
790
|
+
<span className="text-sm text-foreground truncate flex-1">
|
|
791
|
+
{span.span_id}
|
|
792
|
+
</span>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
{/* Duration */}
|
|
796
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
797
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
798
|
+
Duration
|
|
799
|
+
</span>
|
|
800
|
+
<span className="text-sm text-foreground">
|
|
801
|
+
{formatDuration(span.durationMs)}
|
|
802
|
+
</span>
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
{/* Started */}
|
|
806
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
807
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
808
|
+
Started
|
|
809
|
+
</span>
|
|
810
|
+
<span className="text-sm text-foreground">
|
|
811
|
+
{formatTimestamp(span.start_time_unix_nano)}
|
|
812
|
+
</span>
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
{/* Tokens */}
|
|
816
|
+
{tokenCount > 0 && (
|
|
817
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
818
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
819
|
+
Tokens
|
|
820
|
+
</span>
|
|
821
|
+
<span className="text-sm text-foreground">
|
|
822
|
+
{tokenCount.toLocaleString()}
|
|
823
|
+
</span>
|
|
824
|
+
</div>
|
|
825
|
+
)}
|
|
826
|
+
|
|
827
|
+
{/* Tool Usage - only show for parent spans, not individual tool calls */}
|
|
828
|
+
{callCount > 0 &&
|
|
829
|
+
spanType !== "tool_call" &&
|
|
830
|
+
spanType !== "subagent" && (
|
|
831
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
832
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
833
|
+
Tool Usage
|
|
834
|
+
</span>
|
|
835
|
+
<span className="text-sm text-foreground">
|
|
836
|
+
{toolCount} {toolCount === 1 ? "tool" : "tools"},{" "}
|
|
837
|
+
{callCount} tool {callCount === 1 ? "call" : "calls"}
|
|
838
|
+
</span>
|
|
839
|
+
</div>
|
|
840
|
+
)}
|
|
841
|
+
|
|
842
|
+
{/* Logs */}
|
|
843
|
+
{logCount > 0 && (
|
|
844
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
845
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
846
|
+
Logs
|
|
847
|
+
</span>
|
|
848
|
+
<span className="text-sm text-foreground">{logCount}</span>
|
|
849
|
+
</div>
|
|
850
|
+
)}
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
{/* Tool Call Input/Output - only show for tool_call and subagent spans */}
|
|
854
|
+
{(spanType === "tool_call" || spanType === "subagent") && (
|
|
855
|
+
<>
|
|
856
|
+
<JsonSection title="Input" data={attrs["tool.input"]} />
|
|
857
|
+
<JsonSection title="Output" data={attrs["tool.output"]} />
|
|
858
|
+
</>
|
|
859
|
+
)}
|
|
860
|
+
|
|
861
|
+
{/* Chat Messages - only show for chat spans */}
|
|
862
|
+
{spanType === "chat" && (
|
|
863
|
+
<>
|
|
864
|
+
<MessageSection
|
|
865
|
+
title="Input Messages"
|
|
866
|
+
data={attrs["gen_ai.input.messages"]}
|
|
867
|
+
/>
|
|
868
|
+
<MessageSection
|
|
869
|
+
title="Output Messages"
|
|
870
|
+
data={attrs["gen_ai.output.messages"]}
|
|
871
|
+
/>
|
|
872
|
+
</>
|
|
873
|
+
)}
|
|
874
|
+
|
|
875
|
+
{/* Attributes */}
|
|
876
|
+
{(() => {
|
|
877
|
+
// Filter out span-specific keys
|
|
878
|
+
let keysToExclude: string[] = [];
|
|
879
|
+
|
|
880
|
+
if (spanType === "tool_call" || spanType === "subagent") {
|
|
881
|
+
keysToExclude = ["tool.name", "tool.input", "tool.output"];
|
|
882
|
+
} else if (spanType === "chat") {
|
|
883
|
+
keysToExclude = [
|
|
884
|
+
"gen_ai.input.messages",
|
|
885
|
+
"gen_ai.output.messages",
|
|
886
|
+
];
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const filteredAttrs = { ...attrs };
|
|
890
|
+
for (const key of keysToExclude) {
|
|
891
|
+
delete filteredAttrs[key];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Only show if there are attributes remaining
|
|
895
|
+
return Object.keys(filteredAttrs).length > 0 ? (
|
|
896
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
897
|
+
<div className="p-2 border-b border-border">
|
|
898
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
899
|
+
Attributes
|
|
900
|
+
</h3>
|
|
901
|
+
</div>
|
|
902
|
+
<div className="p-3">
|
|
903
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
904
|
+
{JSON.stringify(filteredAttrs, null, 2)}
|
|
905
|
+
</pre>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
) : null;
|
|
909
|
+
})()}
|
|
910
|
+
|
|
911
|
+
{/* Resource */}
|
|
912
|
+
{resourceAttrs && Object.keys(resourceAttrs).length > 0 && (
|
|
913
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
914
|
+
<div className="p-2 border-b border-border">
|
|
915
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
916
|
+
Resource
|
|
917
|
+
</h3>
|
|
918
|
+
</div>
|
|
919
|
+
<div className="p-3">
|
|
920
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
921
|
+
{JSON.stringify(resourceAttrs, null, 2)}
|
|
922
|
+
</pre>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
)}
|
|
926
|
+
</div>
|
|
927
|
+
)}
|
|
928
|
+
|
|
929
|
+
{activeTab === "logs" && (
|
|
930
|
+
<div className="text-sm text-muted-foreground">
|
|
931
|
+
Logs view - Coming soon
|
|
932
|
+
</div>
|
|
933
|
+
)}
|
|
934
|
+
|
|
935
|
+
{activeTab === "feedback" && (
|
|
936
|
+
<div className="text-sm text-muted-foreground">
|
|
937
|
+
Feedback view - Coming soon
|
|
938
|
+
</div>
|
|
939
|
+
)}
|
|
940
|
+
</div>
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function LogDetails({ log }: { log: Log }) {
|
|
945
|
+
const attrs = parseAttributes(log.attributes);
|
|
946
|
+
const resourceAttrs = parseAttributes(log.resource_attributes);
|
|
947
|
+
|
|
948
|
+
const severityColor =
|
|
949
|
+
log.severity_number >= 17
|
|
950
|
+
? "text-red-500"
|
|
951
|
+
: log.severity_number >= 13
|
|
952
|
+
? "text-yellow-500"
|
|
953
|
+
: "text-muted-foreground";
|
|
954
|
+
|
|
955
|
+
return (
|
|
956
|
+
<div className="flex flex-col gap-4">
|
|
957
|
+
{/* Details section */}
|
|
958
|
+
<div className="flex flex-col">
|
|
959
|
+
{/* Timestamp */}
|
|
960
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
961
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
962
|
+
Timestamp
|
|
963
|
+
</span>
|
|
964
|
+
<span className="text-sm text-foreground">
|
|
965
|
+
{formatTimestamp(log.timestamp_unix_nano)}
|
|
966
|
+
</span>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
{/* Severity */}
|
|
970
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
971
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
972
|
+
Severity
|
|
973
|
+
</span>
|
|
974
|
+
<span className={cn("text-sm font-medium", severityColor)}>
|
|
975
|
+
{log.severity_text || `Level ${log.severity_number}`}
|
|
976
|
+
</span>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
{/* Trace ID */}
|
|
980
|
+
{log.trace_id && (
|
|
981
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
982
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
983
|
+
Trace ID
|
|
984
|
+
</span>
|
|
985
|
+
<span className="text-sm text-foreground truncate flex-1 font-mono">
|
|
986
|
+
{log.trace_id}
|
|
987
|
+
</span>
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
|
|
991
|
+
{/* Span ID */}
|
|
992
|
+
{log.span_id && (
|
|
993
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
994
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
995
|
+
Span ID
|
|
996
|
+
</span>
|
|
997
|
+
<span className="text-sm text-foreground truncate flex-1 font-mono">
|
|
998
|
+
{log.span_id}
|
|
999
|
+
</span>
|
|
1000
|
+
</div>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
|
|
1004
|
+
{/* Body */}
|
|
1005
|
+
{log.body && (
|
|
1006
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
1007
|
+
<div className="p-2 border-b border-border">
|
|
1008
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
1009
|
+
Message
|
|
1010
|
+
</h3>
|
|
1011
|
+
</div>
|
|
1012
|
+
<div className="p-3">
|
|
1013
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
1014
|
+
{log.body}
|
|
1015
|
+
</pre>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
)}
|
|
1019
|
+
|
|
1020
|
+
{/* Attributes */}
|
|
1021
|
+
{attrs && Object.keys(attrs).length > 0 && (
|
|
1022
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
1023
|
+
<div className="p-2 border-b border-border">
|
|
1024
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
1025
|
+
Attributes
|
|
1026
|
+
</h3>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div className="p-3">
|
|
1029
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
1030
|
+
{JSON.stringify(attrs, null, 2)}
|
|
1031
|
+
</pre>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
)}
|
|
1035
|
+
|
|
1036
|
+
{/* Resource Attributes */}
|
|
1037
|
+
{resourceAttrs && Object.keys(resourceAttrs).length > 0 && (
|
|
1038
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
1039
|
+
<div className="p-2 border-b border-border">
|
|
1040
|
+
<h3 className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
1041
|
+
Resource
|
|
1042
|
+
</h3>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div className="p-3">
|
|
1045
|
+
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-all">
|
|
1046
|
+
{JSON.stringify(resourceAttrs, null, 2)}
|
|
1047
|
+
</pre>
|
|
1048
|
+
</div>
|
|
1049
|
+
</div>
|
|
1050
|
+
)}
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function DiagnosticDetailsPanel({
|
|
1056
|
+
span,
|
|
1057
|
+
log,
|
|
1058
|
+
onClose,
|
|
1059
|
+
allSpans,
|
|
1060
|
+
}: DiagnosticDetailsPanelProps) {
|
|
1061
|
+
// Determine which type of detail to show
|
|
1062
|
+
const hasSelection = span !== null || log !== null;
|
|
1063
|
+
|
|
1064
|
+
if (!hasSelection) return null;
|
|
1065
|
+
|
|
1066
|
+
const title = span ? getDisplayName(span) : "Log Details";
|
|
1067
|
+
|
|
1068
|
+
return (
|
|
1069
|
+
<div className="flex flex-col h-full border-l border-border bg-background">
|
|
1070
|
+
{/* Header */}
|
|
1071
|
+
<div className="h-12 px-4 flex items-center justify-between border-b border-border bg-muted/30 shrink-0">
|
|
1072
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
1073
|
+
{span && <SpanIcon span={span} className="size-5 shrink-0" />}
|
|
1074
|
+
<h2 className="text-sm font-medium text-foreground truncate">
|
|
1075
|
+
{title}
|
|
1076
|
+
</h2>
|
|
1077
|
+
</div>
|
|
1078
|
+
<button
|
|
1079
|
+
type="button"
|
|
1080
|
+
onClick={onClose}
|
|
1081
|
+
className="size-7 flex items-center justify-center rounded hover:bg-muted transition-colors shrink-0"
|
|
1082
|
+
aria-label="Close"
|
|
1083
|
+
>
|
|
1084
|
+
<X className="size-4 text-muted-foreground" />
|
|
1085
|
+
</button>
|
|
1086
|
+
</div>
|
|
1087
|
+
|
|
1088
|
+
{/* Content */}
|
|
1089
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
1090
|
+
{span && <SpanDetails span={span} allSpans={allSpans} />}
|
|
1091
|
+
{log && !span && <LogDetails log={log} />}
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
);
|
|
1095
|
+
}
|