@townco/debugger 0.1.13 → 0.1.21
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 +4 -4
- package/src/components/SpanDetailsPanel.tsx +613 -25
- package/src/server.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/debugger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3.0"
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"@radix-ui/react-select": "^2.2.6",
|
|
20
20
|
"@radix-ui/react-slot": "^1.2.3",
|
|
21
21
|
"@radix-ui/react-tabs": "^1.1.0",
|
|
22
|
-
"@townco/otlp-server": "0.1.
|
|
23
|
-
"@townco/ui": "0.1.
|
|
22
|
+
"@townco/otlp-server": "0.1.21",
|
|
23
|
+
"@townco/ui": "0.1.66",
|
|
24
24
|
"bun-plugin-tailwind": "^0.1.2",
|
|
25
25
|
"class-variance-authority": "^0.7.1",
|
|
26
26
|
"clsx": "^2.1.1",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tailwind-merge": "^3.3.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@townco/tsconfig": "0.1.
|
|
33
|
+
"@townco/tsconfig": "0.1.63",
|
|
34
34
|
"@types/bun": "latest",
|
|
35
35
|
"@types/react": "^19",
|
|
36
36
|
"@types/react-dom": "^19",
|
|
@@ -168,6 +168,550 @@ export function SpanDetailsPanel({
|
|
|
168
168
|
const logCount = countLogs(span);
|
|
169
169
|
const tokenCount = countTokens(span);
|
|
170
170
|
|
|
171
|
+
const JsonSection = ({ title, data }: { title: string; data: unknown }) => {
|
|
172
|
+
if (!data) return null;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const jsonString = typeof data === "string" ? data : JSON.stringify(data);
|
|
176
|
+
const parsed = JSON.parse(jsonString);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
180
|
+
<div className="p-3 border-b border-border">
|
|
181
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
182
|
+
{title}
|
|
183
|
+
</h3>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="p-3">
|
|
186
|
+
<pre className="text-[11px] font-mono text-foreground leading-[14px] whitespace-pre-wrap break-all">
|
|
187
|
+
{JSON.stringify(parsed, null, 2)}
|
|
188
|
+
</pre>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
// If parse fails, show raw data
|
|
194
|
+
return (
|
|
195
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
196
|
+
<div className="p-3 border-b border-border">
|
|
197
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
198
|
+
{title}
|
|
199
|
+
</h3>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="p-3">
|
|
202
|
+
<pre className="text-[11px] font-mono text-foreground leading-[14px] whitespace-pre-wrap break-all">
|
|
203
|
+
{String(data)}
|
|
204
|
+
</pre>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const CollapsibleText = ({ text }: { text: string }) => {
|
|
212
|
+
const [expanded, setExpanded] = useState(false);
|
|
213
|
+
const shouldCollapse = text.length > 300;
|
|
214
|
+
const preview = shouldCollapse ? text.slice(0, 300) + "..." : text;
|
|
215
|
+
|
|
216
|
+
if (!shouldCollapse) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="text-[11px] text-foreground whitespace-pre-wrap break-words">
|
|
219
|
+
{text}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div>
|
|
226
|
+
<div className="text-[11px] text-foreground whitespace-pre-wrap break-words">
|
|
227
|
+
{expanded ? text : preview}
|
|
228
|
+
</div>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={() => setExpanded(!expanded)}
|
|
232
|
+
className="text-[10px] text-purple-600 hover:text-purple-700 mt-1 font-medium"
|
|
233
|
+
>
|
|
234
|
+
{expanded ? "Show less" : "Show more"}
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const CollapsibleToolBlock = ({
|
|
241
|
+
title,
|
|
242
|
+
subtitle,
|
|
243
|
+
content,
|
|
244
|
+
borderColor,
|
|
245
|
+
bgColor,
|
|
246
|
+
badgeColor,
|
|
247
|
+
}: {
|
|
248
|
+
title: string;
|
|
249
|
+
subtitle?: string;
|
|
250
|
+
content: any;
|
|
251
|
+
borderColor: string;
|
|
252
|
+
bgColor: string;
|
|
253
|
+
badgeColor: string;
|
|
254
|
+
}) => {
|
|
255
|
+
const [expanded, setExpanded] = useState(false);
|
|
256
|
+
|
|
257
|
+
// Parse content as JSON if it's a string
|
|
258
|
+
let displayContent = content;
|
|
259
|
+
if (typeof content === "string") {
|
|
260
|
+
try {
|
|
261
|
+
displayContent = JSON.parse(content);
|
|
262
|
+
} catch {
|
|
263
|
+
// Keep as string if parsing fails
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const contentString =
|
|
268
|
+
typeof displayContent === "string"
|
|
269
|
+
? displayContent
|
|
270
|
+
: JSON.stringify(displayContent, null, 2);
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className={`border-l-2 ${borderColor} pl-2 ${bgColor} rounded py-1`}>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={() => setExpanded(!expanded)}
|
|
277
|
+
className="flex items-center gap-2 mb-1 w-full hover:opacity-80 transition-opacity"
|
|
278
|
+
>
|
|
279
|
+
<span className={`text-[9px] font-semibold ${badgeColor} uppercase`}>
|
|
280
|
+
{title}
|
|
281
|
+
</span>
|
|
282
|
+
{subtitle && (
|
|
283
|
+
<span className="text-[10px] text-foreground font-mono">
|
|
284
|
+
{subtitle}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
<span className="text-[10px] text-muted-foreground ml-auto">
|
|
288
|
+
{expanded ? "▼" : "▶"}
|
|
289
|
+
</span>
|
|
290
|
+
</button>
|
|
291
|
+
{expanded && (
|
|
292
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all mt-1">
|
|
293
|
+
{contentString}
|
|
294
|
+
</pre>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const ToolCallWithResult = ({
|
|
301
|
+
toolName,
|
|
302
|
+
input,
|
|
303
|
+
result,
|
|
304
|
+
}: {
|
|
305
|
+
toolName: string;
|
|
306
|
+
input: any;
|
|
307
|
+
result?: any;
|
|
308
|
+
}) => {
|
|
309
|
+
const [expanded, setExpanded] = useState(false);
|
|
310
|
+
|
|
311
|
+
const parseContent = (content: any) => {
|
|
312
|
+
if (typeof content === "string") {
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(content);
|
|
315
|
+
} catch {
|
|
316
|
+
return content;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return content;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const formatContent = (content: any) => {
|
|
323
|
+
const parsed = parseContent(content);
|
|
324
|
+
return typeof parsed === "string"
|
|
325
|
+
? parsed
|
|
326
|
+
: JSON.stringify(parsed, null, 2);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div className="border-l-2 border-blue-500 pl-2 bg-blue-500/5 rounded py-1">
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onClick={() => setExpanded(!expanded)}
|
|
334
|
+
className="flex items-center gap-2 mb-1 w-full hover:opacity-80 transition-opacity"
|
|
335
|
+
>
|
|
336
|
+
<span className="text-[9px] font-semibold text-blue-600 uppercase">
|
|
337
|
+
Tool Call
|
|
338
|
+
</span>
|
|
339
|
+
<span className="text-[10px] text-foreground font-mono">
|
|
340
|
+
{toolName}
|
|
341
|
+
</span>
|
|
342
|
+
<span className="text-[10px] text-muted-foreground ml-auto">
|
|
343
|
+
{expanded ? "▼" : "▶"}
|
|
344
|
+
</span>
|
|
345
|
+
</button>
|
|
346
|
+
{expanded && (
|
|
347
|
+
<div className="flex flex-col gap-2 mt-1">
|
|
348
|
+
{/* Input section */}
|
|
349
|
+
<div>
|
|
350
|
+
<div className="text-[9px] font-semibold text-purple-600 uppercase mb-1">
|
|
351
|
+
Input
|
|
352
|
+
</div>
|
|
353
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all bg-background/50 rounded p-2">
|
|
354
|
+
{formatContent(input)}
|
|
355
|
+
</pre>
|
|
356
|
+
</div>
|
|
357
|
+
{/* Result section */}
|
|
358
|
+
{result !== undefined && (
|
|
359
|
+
<div>
|
|
360
|
+
<div className="text-[9px] font-semibold text-green-600 uppercase mb-1">
|
|
361
|
+
Result
|
|
362
|
+
</div>
|
|
363
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all bg-background/50 rounded p-2">
|
|
364
|
+
{formatContent(result)}
|
|
365
|
+
</pre>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const MessageSection = ({
|
|
375
|
+
title,
|
|
376
|
+
data,
|
|
377
|
+
}: {
|
|
378
|
+
title: string;
|
|
379
|
+
data: unknown;
|
|
380
|
+
}) => {
|
|
381
|
+
if (!data) return null;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const jsonString = typeof data === "string" ? data : JSON.stringify(data);
|
|
385
|
+
const messages = JSON.parse(jsonString);
|
|
386
|
+
|
|
387
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
388
|
+
|
|
389
|
+
const parseToolInput = (input: any): any => {
|
|
390
|
+
if (typeof input === "string") {
|
|
391
|
+
try {
|
|
392
|
+
return JSON.parse(input);
|
|
393
|
+
} catch {
|
|
394
|
+
return input;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return input;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const renderContent = (content: any) => {
|
|
401
|
+
// Handle string content - parse if it looks like JSON array
|
|
402
|
+
if (typeof content === "string") {
|
|
403
|
+
// Try to parse if it looks like a JSON array
|
|
404
|
+
if (content.trim().startsWith("[")) {
|
|
405
|
+
try {
|
|
406
|
+
const parsed = JSON.parse(content);
|
|
407
|
+
if (Array.isArray(parsed)) {
|
|
408
|
+
return renderContent(parsed);
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
// Not valid JSON, fall through to text display
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return <CollapsibleText text={content} />;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Handle array content (can contain text and tool_use blocks)
|
|
419
|
+
if (Array.isArray(content)) {
|
|
420
|
+
// First, group tool_use with their corresponding tool_result
|
|
421
|
+
const processedBlocks: any[] = [];
|
|
422
|
+
const usedResultIndices = new Set<number>();
|
|
423
|
+
|
|
424
|
+
content.forEach((block: any, blockIndex: number) => {
|
|
425
|
+
if (block.type === "text") {
|
|
426
|
+
processedBlocks.push({ type: "text", block, index: blockIndex });
|
|
427
|
+
} else if (block.type === "tool_use") {
|
|
428
|
+
// Find the corresponding tool_result
|
|
429
|
+
const resultIndex = content.findIndex(
|
|
430
|
+
(b: any, idx: number) =>
|
|
431
|
+
b.type === "tool_result" &&
|
|
432
|
+
b.tool_use_id === block.id &&
|
|
433
|
+
!usedResultIndices.has(idx),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (resultIndex !== -1) {
|
|
437
|
+
usedResultIndices.add(resultIndex);
|
|
438
|
+
processedBlocks.push({
|
|
439
|
+
type: "tool_call_with_result",
|
|
440
|
+
toolUse: block,
|
|
441
|
+
toolResult: content[resultIndex],
|
|
442
|
+
index: blockIndex,
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
// No result found, show just the tool_use
|
|
446
|
+
processedBlocks.push({
|
|
447
|
+
type: "tool_use",
|
|
448
|
+
block,
|
|
449
|
+
index: blockIndex,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
} else if (
|
|
453
|
+
block.type === "tool_result" &&
|
|
454
|
+
!usedResultIndices.has(blockIndex)
|
|
455
|
+
) {
|
|
456
|
+
// Orphaned tool_result (no matching tool_use)
|
|
457
|
+
processedBlocks.push({
|
|
458
|
+
type: "tool_result",
|
|
459
|
+
block,
|
|
460
|
+
index: blockIndex,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return (
|
|
466
|
+
<div className="flex flex-col gap-2">
|
|
467
|
+
{processedBlocks.map((item: any) => {
|
|
468
|
+
if (item.type === "text") {
|
|
469
|
+
return (
|
|
470
|
+
<CollapsibleText key={item.index} text={item.block.text} />
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (item.type === "tool_call_with_result") {
|
|
475
|
+
const parsedInput = parseToolInput(item.toolUse.input);
|
|
476
|
+
return (
|
|
477
|
+
<ToolCallWithResult
|
|
478
|
+
key={item.index}
|
|
479
|
+
toolName={item.toolUse.name}
|
|
480
|
+
input={parsedInput}
|
|
481
|
+
result={item.toolResult.content}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (item.type === "tool_use") {
|
|
487
|
+
const parsedInput = parseToolInput(item.block.input);
|
|
488
|
+
return (
|
|
489
|
+
<CollapsibleToolBlock
|
|
490
|
+
key={item.index}
|
|
491
|
+
title="Tool Use"
|
|
492
|
+
subtitle={item.block.name}
|
|
493
|
+
content={parsedInput}
|
|
494
|
+
borderColor="border-purple-500"
|
|
495
|
+
bgColor="bg-purple-500/5"
|
|
496
|
+
badgeColor="text-purple-600"
|
|
497
|
+
/>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (item.type === "tool_result") {
|
|
502
|
+
return (
|
|
503
|
+
<CollapsibleToolBlock
|
|
504
|
+
key={item.index}
|
|
505
|
+
title="Tool Result"
|
|
506
|
+
subtitle=""
|
|
507
|
+
content={item.block.content}
|
|
508
|
+
borderColor="border-green-500"
|
|
509
|
+
bgColor="bg-green-500/5"
|
|
510
|
+
badgeColor="text-green-600"
|
|
511
|
+
/>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Unknown block type - show as JSON
|
|
516
|
+
return (
|
|
517
|
+
<pre
|
|
518
|
+
key={item.index}
|
|
519
|
+
className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all"
|
|
520
|
+
>
|
|
521
|
+
{JSON.stringify(item.block, null, 2)}
|
|
522
|
+
</pre>
|
|
523
|
+
);
|
|
524
|
+
})}
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Fallback for other content types
|
|
530
|
+
return (
|
|
531
|
+
<pre className="text-[10px] font-mono text-muted-foreground whitespace-pre-wrap break-all">
|
|
532
|
+
{JSON.stringify(content, null, 2)}
|
|
533
|
+
</pre>
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
539
|
+
<div className="p-3 border-b border-border">
|
|
540
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
541
|
+
{title}
|
|
542
|
+
</h3>
|
|
543
|
+
</div>
|
|
544
|
+
<div className="p-3 flex flex-col gap-3">
|
|
545
|
+
{(() => {
|
|
546
|
+
// Group tool_use (from AI messages) with tool results (from tool messages)
|
|
547
|
+
const processedMessages: any[] = [];
|
|
548
|
+
const usedToolMessageIndices = new Set<number>();
|
|
549
|
+
|
|
550
|
+
messages.forEach((message: any, msgIndex: number) => {
|
|
551
|
+
const role = message.role || "unknown";
|
|
552
|
+
|
|
553
|
+
// Handle AI messages - check for tool_use blocks
|
|
554
|
+
if (role === "ai") {
|
|
555
|
+
const content = message.content;
|
|
556
|
+
let parsedContent = content;
|
|
557
|
+
|
|
558
|
+
if (
|
|
559
|
+
typeof content === "string" &&
|
|
560
|
+
content.trim().startsWith("[")
|
|
561
|
+
) {
|
|
562
|
+
try {
|
|
563
|
+
parsedContent = JSON.parse(content);
|
|
564
|
+
} catch {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (Array.isArray(parsedContent)) {
|
|
568
|
+
// Extract tool_use blocks
|
|
569
|
+
const toolUses: any[] = [];
|
|
570
|
+
const nonToolBlocks: any[] = [];
|
|
571
|
+
|
|
572
|
+
for (const block of parsedContent) {
|
|
573
|
+
if (block.type === "tool_use") {
|
|
574
|
+
toolUses.push(block);
|
|
575
|
+
} else {
|
|
576
|
+
nonToolBlocks.push(block);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Add non-tool content if any
|
|
581
|
+
if (nonToolBlocks.length > 0) {
|
|
582
|
+
processedMessages.push({
|
|
583
|
+
type: "content",
|
|
584
|
+
role,
|
|
585
|
+
content: nonToolBlocks,
|
|
586
|
+
index: msgIndex,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// For each tool_use, find the matching tool message
|
|
591
|
+
for (const toolUse of toolUses) {
|
|
592
|
+
// Find next unused tool message
|
|
593
|
+
let foundToolMessage = false;
|
|
594
|
+
for (let i = msgIndex + 1; i < messages.length; i++) {
|
|
595
|
+
if (
|
|
596
|
+
messages[i].role === "tool" &&
|
|
597
|
+
!usedToolMessageIndices.has(i)
|
|
598
|
+
) {
|
|
599
|
+
usedToolMessageIndices.add(i);
|
|
600
|
+
processedMessages.push({
|
|
601
|
+
type: "tool_call_with_result",
|
|
602
|
+
toolUse: toolUse,
|
|
603
|
+
toolResult: messages[i].content,
|
|
604
|
+
index: msgIndex,
|
|
605
|
+
});
|
|
606
|
+
foundToolMessage = true;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// If no tool message found, show just the tool_use
|
|
612
|
+
if (!foundToolMessage) {
|
|
613
|
+
processedMessages.push({
|
|
614
|
+
type: "tool_use_only",
|
|
615
|
+
toolUse: toolUse,
|
|
616
|
+
index: msgIndex,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
// Non-array content, render normally
|
|
622
|
+
processedMessages.push({
|
|
623
|
+
type: "content",
|
|
624
|
+
role,
|
|
625
|
+
content: parsedContent,
|
|
626
|
+
index: msgIndex,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
} else if (role === "tool") {
|
|
630
|
+
// Skip if already matched, otherwise show as orphan result
|
|
631
|
+
if (!usedToolMessageIndices.has(msgIndex)) {
|
|
632
|
+
processedMessages.push({
|
|
633
|
+
type: "tool_result_only",
|
|
634
|
+
content: message.content,
|
|
635
|
+
index: msgIndex,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// Other roles (system, human, user, etc.)
|
|
640
|
+
processedMessages.push({
|
|
641
|
+
type: "content",
|
|
642
|
+
role,
|
|
643
|
+
content: message.content,
|
|
644
|
+
index: msgIndex,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Render processed messages
|
|
650
|
+
return processedMessages.map((item, idx) => {
|
|
651
|
+
if (item.type === "tool_call_with_result") {
|
|
652
|
+
return (
|
|
653
|
+
<ToolCallWithResult
|
|
654
|
+
key={`${item.index}-${idx}`}
|
|
655
|
+
toolName={item.toolUse.name}
|
|
656
|
+
input={parseToolInput(item.toolUse.input)}
|
|
657
|
+
result={item.toolResult}
|
|
658
|
+
/>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (item.type === "tool_use_only") {
|
|
663
|
+
return (
|
|
664
|
+
<CollapsibleToolBlock
|
|
665
|
+
key={`${item.index}-${idx}`}
|
|
666
|
+
title="Tool Use"
|
|
667
|
+
subtitle={item.toolUse.name}
|
|
668
|
+
content={parseToolInput(item.toolUse.input)}
|
|
669
|
+
borderColor="border-purple-500"
|
|
670
|
+
bgColor="bg-purple-500/5"
|
|
671
|
+
badgeColor="text-purple-600"
|
|
672
|
+
/>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (item.type === "tool_result_only") {
|
|
677
|
+
return (
|
|
678
|
+
<CollapsibleToolBlock
|
|
679
|
+
key={`${item.index}-${idx}`}
|
|
680
|
+
title="Tool Result"
|
|
681
|
+
subtitle=""
|
|
682
|
+
content={item.content}
|
|
683
|
+
borderColor="border-green-500"
|
|
684
|
+
bgColor="bg-green-500/5"
|
|
685
|
+
badgeColor="text-green-600"
|
|
686
|
+
/>
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Regular content
|
|
691
|
+
return (
|
|
692
|
+
<div
|
|
693
|
+
key={`${item.index}-${idx}`}
|
|
694
|
+
className="border border-border rounded p-2 bg-background"
|
|
695
|
+
>
|
|
696
|
+
<div className="flex items-center gap-2 mb-2">
|
|
697
|
+
<span className="text-[10px] font-semibold text-muted-foreground uppercase">
|
|
698
|
+
{item.role}
|
|
699
|
+
</span>
|
|
700
|
+
</div>
|
|
701
|
+
{renderContent(item.content)}
|
|
702
|
+
</div>
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
})()}
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
);
|
|
709
|
+
} catch {
|
|
710
|
+
// If parse fails, fall back to JSON display
|
|
711
|
+
return <JsonSection title={title} data={data} />;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
171
715
|
return (
|
|
172
716
|
<Dialog.Root open={!!span} onOpenChange={(open) => !open && onClose()}>
|
|
173
717
|
<Dialog.Portal>
|
|
@@ -292,18 +836,20 @@ export function SpanDetailsPanel({
|
|
|
292
836
|
</div>
|
|
293
837
|
)}
|
|
294
838
|
|
|
295
|
-
{/* Tool Usage */}
|
|
296
|
-
{callCount > 0 &&
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
839
|
+
{/* Tool Usage - only show for parent spans, not individual tool calls */}
|
|
840
|
+
{callCount > 0 &&
|
|
841
|
+
spanType !== "tool_call" &&
|
|
842
|
+
spanType !== "subagent" && (
|
|
843
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
844
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
845
|
+
Tool Usage
|
|
846
|
+
</span>
|
|
847
|
+
<span className="text-sm text-foreground">
|
|
848
|
+
{toolCount} {toolCount === 1 ? "tool" : "tools"},{" "}
|
|
849
|
+
{callCount} tool {callCount === 1 ? "call" : "calls"}
|
|
850
|
+
</span>
|
|
851
|
+
</div>
|
|
852
|
+
)}
|
|
307
853
|
|
|
308
854
|
{/* Logs */}
|
|
309
855
|
{logCount > 0 && (
|
|
@@ -318,21 +864,63 @@ export function SpanDetailsPanel({
|
|
|
318
864
|
)}
|
|
319
865
|
</div>
|
|
320
866
|
|
|
867
|
+
{/* Tool Call Input/Output - only show for tool_call and subagent spans */}
|
|
868
|
+
{(spanType === "tool_call" || spanType === "subagent") && (
|
|
869
|
+
<>
|
|
870
|
+
<JsonSection title="Input" data={attrs["tool.input"]} />
|
|
871
|
+
<JsonSection title="Output" data={attrs["tool.output"]} />
|
|
872
|
+
</>
|
|
873
|
+
)}
|
|
874
|
+
|
|
875
|
+
{/* Chat Messages - only show for chat spans */}
|
|
876
|
+
{spanType === "chat" && (
|
|
877
|
+
<>
|
|
878
|
+
<MessageSection
|
|
879
|
+
title="Input Messages"
|
|
880
|
+
data={attrs["gen_ai.input.messages"]}
|
|
881
|
+
/>
|
|
882
|
+
<MessageSection
|
|
883
|
+
title="Output Messages"
|
|
884
|
+
data={attrs["gen_ai.output.messages"]}
|
|
885
|
+
/>
|
|
886
|
+
</>
|
|
887
|
+
)}
|
|
888
|
+
|
|
321
889
|
{/* Attributes */}
|
|
322
|
-
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
890
|
+
{(() => {
|
|
891
|
+
// Filter out span-specific keys
|
|
892
|
+
let keysToExclude: string[] = [];
|
|
893
|
+
|
|
894
|
+
if (spanType === "tool_call" || spanType === "subagent") {
|
|
895
|
+
keysToExclude = ["tool.name", "tool.input", "tool.output"];
|
|
896
|
+
} else if (spanType === "chat") {
|
|
897
|
+
keysToExclude = [
|
|
898
|
+
"gen_ai.input.messages",
|
|
899
|
+
"gen_ai.output.messages",
|
|
900
|
+
];
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const filteredAttrs = { ...attrs };
|
|
904
|
+
for (const key of keysToExclude) {
|
|
905
|
+
delete filteredAttrs[key];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Only show if there are attributes remaining
|
|
909
|
+
return Object.keys(filteredAttrs).length > 0 ? (
|
|
910
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
911
|
+
<div className="p-3 border-b border-border">
|
|
912
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
913
|
+
Attributes
|
|
914
|
+
</h3>
|
|
915
|
+
</div>
|
|
916
|
+
<div className="p-3">
|
|
917
|
+
<pre className="text-[11px] font-mono text-foreground leading-[14px] whitespace-pre-wrap break-all">
|
|
918
|
+
{JSON.stringify(filteredAttrs, null, 2)}
|
|
919
|
+
</pre>
|
|
920
|
+
</div>
|
|
333
921
|
</div>
|
|
334
|
-
|
|
335
|
-
)}
|
|
922
|
+
) : null;
|
|
923
|
+
})()}
|
|
336
924
|
|
|
337
925
|
{/* Resource */}
|
|
338
926
|
{resourceAttrs && Object.keys(resourceAttrs).length > 0 && (
|