@townco/debugger 0.1.5 → 0.1.7
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 +6 -3
- package/src/App.tsx +12 -3
- package/src/components/DebuggerHeader.tsx +85 -0
- package/src/components/DebuggerLayout.tsx +30 -0
- package/src/components/LogList.tsx +2 -2
- package/src/components/MessageBubble.tsx +28 -0
- package/src/components/SessionTraceList.tsx +38 -33
- package/src/components/SpanDetailsPanel.tsx +371 -0
- package/src/components/SpanIcon.tsx +42 -0
- package/src/components/SpanTimeline.tsx +450 -0
- package/src/components/SpanTree.tsx +28 -8
- package/src/components/TraceDetailContent.tsx +97 -68
- package/src/components/TurnHeader.tsx +50 -0
- package/src/components/TurnMetadataPanel.tsx +63 -0
- package/src/components/ui/tabs.tsx +50 -0
- package/src/db.ts +33 -10
- package/src/index.ts +7 -1
- package/src/lib/spanTypeDetector.ts +35 -0
- package/src/lib/timelineCalculator.ts +32 -0
- package/src/lib/turnExtractor.ts +132 -0
- package/src/pages/SessionList.tsx +142 -0
- package/src/pages/SessionView.tsx +8 -19
- package/src/pages/TraceDetail.tsx +5 -8
- package/src/pages/TraceList.tsx +66 -61
- package/src/server.ts +53 -1
- package/src/types.ts +23 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { detectSpanType } from "../lib/spanTypeDetector";
|
|
6
|
+
import type { SpanNode } from "../types";
|
|
7
|
+
import { SpanIcon } from "./SpanIcon";
|
|
8
|
+
|
|
9
|
+
interface SpanDetailsPanelProps {
|
|
10
|
+
span: SpanNode | 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
|
+
export function SpanDetailsPanel({
|
|
152
|
+
span,
|
|
153
|
+
onClose,
|
|
154
|
+
allSpans,
|
|
155
|
+
}: SpanDetailsPanelProps) {
|
|
156
|
+
const [activeTab, setActiveTab] = useState<"run" | "logs" | "feedback">(
|
|
157
|
+
"run",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!span) return null;
|
|
161
|
+
|
|
162
|
+
const spanType = detectSpanType(span);
|
|
163
|
+
const displayName = getDisplayName(span);
|
|
164
|
+
const parentSpan = findParentSpan(span, allSpans);
|
|
165
|
+
const attrs = parseAttributes(span.attributes);
|
|
166
|
+
const resourceAttrs = parseAttributes(span.resource_attributes);
|
|
167
|
+
const { toolCount, callCount } = countToolUsage(span);
|
|
168
|
+
const logCount = countLogs(span);
|
|
169
|
+
const tokenCount = countTokens(span);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Dialog.Root open={!!span} onOpenChange={(open) => !open && onClose()}>
|
|
173
|
+
<Dialog.Portal>
|
|
174
|
+
<Dialog.Overlay className="fixed inset-0 bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
|
175
|
+
<Dialog.Content className="fixed right-0 top-0 h-full w-[600px] bg-background shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right duration-200 overflow-y-auto border-l border-border">
|
|
176
|
+
<div className="flex flex-col gap-6 px-6 py-8">
|
|
177
|
+
{/* Header */}
|
|
178
|
+
<div className="flex items-start gap-2">
|
|
179
|
+
<SpanIcon type={spanType} className="size-6" />
|
|
180
|
+
<h2 className="flex-1 text-xl font-semibold text-foreground tracking-[-0.02em] leading-tight overflow-hidden text-ellipsis">
|
|
181
|
+
{displayName}
|
|
182
|
+
</h2>
|
|
183
|
+
<Dialog.Close asChild>
|
|
184
|
+
<button
|
|
185
|
+
className="size-6 flex items-center justify-center rounded hover:bg-muted transition-colors"
|
|
186
|
+
aria-label="Close"
|
|
187
|
+
>
|
|
188
|
+
<X className="size-4 text-muted-foreground" />
|
|
189
|
+
</button>
|
|
190
|
+
</Dialog.Close>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Tabs */}
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setActiveTab("run")}
|
|
197
|
+
className={cn(
|
|
198
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
199
|
+
activeTab === "run"
|
|
200
|
+
? "bg-muted text-foreground"
|
|
201
|
+
: "text-muted-foreground hover:text-foreground",
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
Run
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => setActiveTab("logs")}
|
|
208
|
+
className={cn(
|
|
209
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
210
|
+
activeTab === "logs"
|
|
211
|
+
? "bg-muted text-foreground"
|
|
212
|
+
: "text-muted-foreground hover:text-foreground",
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
Logs
|
|
216
|
+
</button>
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => setActiveTab("feedback")}
|
|
219
|
+
className={cn(
|
|
220
|
+
"px-3 py-1 text-sm font-medium rounded-lg transition-colors",
|
|
221
|
+
activeTab === "feedback"
|
|
222
|
+
? "bg-muted text-foreground"
|
|
223
|
+
: "text-muted-foreground hover:text-foreground",
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
Feedback
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* Content */}
|
|
231
|
+
{activeTab === "run" && (
|
|
232
|
+
<div className="flex flex-col gap-4">
|
|
233
|
+
{/* Details section */}
|
|
234
|
+
<div className="flex flex-col">
|
|
235
|
+
{/* Parent Span */}
|
|
236
|
+
{parentSpan && (
|
|
237
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
238
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
239
|
+
Parent Span
|
|
240
|
+
</span>
|
|
241
|
+
<div className="flex items-center gap-2 flex-1 overflow-hidden">
|
|
242
|
+
<SpanIcon
|
|
243
|
+
type={detectSpanType(parentSpan)}
|
|
244
|
+
className="size-5 shrink-0"
|
|
245
|
+
/>
|
|
246
|
+
<span className="text-sm text-foreground truncate">
|
|
247
|
+
{getDisplayName(parentSpan)}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Span ID */}
|
|
254
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
255
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
256
|
+
Span ID
|
|
257
|
+
</span>
|
|
258
|
+
<span className="text-sm text-foreground truncate flex-1">
|
|
259
|
+
{span.span_id}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Duration */}
|
|
264
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
265
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
266
|
+
Duration
|
|
267
|
+
</span>
|
|
268
|
+
<span className="text-sm text-foreground">
|
|
269
|
+
{formatDuration(span.durationMs)}
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Started */}
|
|
274
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
275
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
276
|
+
Started
|
|
277
|
+
</span>
|
|
278
|
+
<span className="text-sm text-foreground">
|
|
279
|
+
{formatTimestamp(span.start_time_unix_nano)}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Tokens */}
|
|
284
|
+
{tokenCount > 0 && (
|
|
285
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
286
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
287
|
+
Tokens
|
|
288
|
+
</span>
|
|
289
|
+
<span className="text-sm text-foreground">
|
|
290
|
+
{tokenCount.toLocaleString()}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Tool Usage */}
|
|
296
|
+
{callCount > 0 && (
|
|
297
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
298
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
299
|
+
Tool Usage
|
|
300
|
+
</span>
|
|
301
|
+
<span className="text-sm text-foreground">
|
|
302
|
+
{toolCount} {toolCount === 1 ? "tool" : "tools"},{" "}
|
|
303
|
+
{callCount} tool {callCount === 1 ? "call" : "calls"}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{/* Logs */}
|
|
309
|
+
{logCount > 0 && (
|
|
310
|
+
<div className="border-t border-border flex items-center gap-6 h-9">
|
|
311
|
+
<span className="text-xs text-muted-foreground w-24 shrink-0">
|
|
312
|
+
Logs
|
|
313
|
+
</span>
|
|
314
|
+
<span className="text-sm text-foreground">
|
|
315
|
+
{logCount}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Attributes */}
|
|
322
|
+
{attrs && Object.keys(attrs).length > 0 && (
|
|
323
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
324
|
+
<div className="p-3 border-b border-border">
|
|
325
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
326
|
+
Attributes
|
|
327
|
+
</h3>
|
|
328
|
+
</div>
|
|
329
|
+
<div className="p-3">
|
|
330
|
+
<pre className="text-[11px] font-mono text-foreground leading-[14px] whitespace-pre-wrap break-all">
|
|
331
|
+
{JSON.stringify(attrs, null, 2)}
|
|
332
|
+
</pre>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* Resource */}
|
|
338
|
+
{resourceAttrs && Object.keys(resourceAttrs).length > 0 && (
|
|
339
|
+
<div className="bg-muted/50 border border-border rounded-lg overflow-hidden">
|
|
340
|
+
<div className="p-3 border-b border-border">
|
|
341
|
+
<h3 className="text-[8px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
342
|
+
Resource
|
|
343
|
+
</h3>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="p-3">
|
|
346
|
+
<pre className="text-[11px] font-mono text-foreground leading-[14px] whitespace-pre-wrap break-all">
|
|
347
|
+
{JSON.stringify(resourceAttrs, null, 2)}
|
|
348
|
+
</pre>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{activeTab === "logs" && (
|
|
356
|
+
<div className="text-sm text-muted-foreground">
|
|
357
|
+
Logs view - Coming soon
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
{activeTab === "feedback" && (
|
|
362
|
+
<div className="text-sm text-muted-foreground">
|
|
363
|
+
Feedback view - Coming soon
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
</Dialog.Content>
|
|
368
|
+
</Dialog.Portal>
|
|
369
|
+
</Dialog.Root>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ListChecks, Search, Settings, Sparkles } from "lucide-react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import type { SpanType } from "../lib/spanTypeDetector";
|
|
4
|
+
|
|
5
|
+
interface SpanIconProps {
|
|
6
|
+
type: SpanType;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SpanIcon({ type, className }: SpanIconProps) {
|
|
11
|
+
// Background color for the icon box
|
|
12
|
+
const bgColor =
|
|
13
|
+
type === "chat"
|
|
14
|
+
? "bg-orange-400"
|
|
15
|
+
: type === "tool_call"
|
|
16
|
+
? "bg-blue-500"
|
|
17
|
+
: type === "subagent"
|
|
18
|
+
? "bg-purple-500"
|
|
19
|
+
: "bg-blue-500";
|
|
20
|
+
|
|
21
|
+
// Choose icon based on type
|
|
22
|
+
const Icon =
|
|
23
|
+
type === "chat"
|
|
24
|
+
? Sparkles
|
|
25
|
+
: type === "tool_call"
|
|
26
|
+
? ListChecks
|
|
27
|
+
: type === "subagent"
|
|
28
|
+
? Search
|
|
29
|
+
: Settings;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"size-5 rounded flex items-center justify-center",
|
|
35
|
+
bgColor,
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Icon className="size-3 text-white" />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|