@townco/debugger 0.1.5 → 0.1.6
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/debugger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3.0"
|
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
"check": "tsc --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@
|
|
17
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
18
18
|
"@radix-ui/react-label": "^2.1.7",
|
|
19
19
|
"@radix-ui/react-select": "^2.2.6",
|
|
20
20
|
"@radix-ui/react-slot": "^1.2.3",
|
|
21
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
22
|
+
"@townco/otlp-server": "0.1.6",
|
|
23
|
+
"@townco/ui": "0.1.51",
|
|
21
24
|
"bun-plugin-tailwind": "^0.1.2",
|
|
22
25
|
"class-variance-authority": "^0.7.1",
|
|
23
26
|
"clsx": "^2.1.1",
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
"tailwind-merge": "^3.3.1"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
|
-
"@townco/tsconfig": "0.1.
|
|
33
|
+
"@townco/tsconfig": "0.1.48",
|
|
31
34
|
"@types/bun": "latest",
|
|
32
35
|
"@types/react": "^19",
|
|
33
36
|
"@types/react-dom": "^19",
|
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ThemeProvider } from "@townco/ui/gui";
|
|
1
2
|
import { Component, type ReactNode } from "react";
|
|
2
3
|
import "./index.css";
|
|
4
|
+
import { SessionList } from "./pages/SessionList";
|
|
3
5
|
import { SessionView } from "./pages/SessionView";
|
|
4
6
|
import { TraceDetail } from "./pages/TraceDetail";
|
|
5
7
|
import { TraceList } from "./pages/TraceList";
|
|
@@ -88,14 +90,21 @@ function AppContent() {
|
|
|
88
90
|
return <TraceDetail traceId={traceMatch[1]} />;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
+
// Route: /traces
|
|
94
|
+
if (pathname === "/traces") {
|
|
95
|
+
return <TraceList />;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default: Session list
|
|
99
|
+
return <SessionList />;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
export function App() {
|
|
96
103
|
return (
|
|
97
104
|
<ErrorBoundary>
|
|
98
|
-
<
|
|
105
|
+
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
|
106
|
+
<AppContent />
|
|
107
|
+
</ThemeProvider>
|
|
99
108
|
</ErrorBoundary>
|
|
100
109
|
);
|
|
101
110
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ChatHeader, cn, ThemeToggle } from "@townco/ui/gui";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
interface DebuggerHeaderProps {
|
|
7
|
+
title: string;
|
|
8
|
+
showBackButton?: boolean;
|
|
9
|
+
backHref?: string;
|
|
10
|
+
showNav?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DebuggerHeader({
|
|
14
|
+
title,
|
|
15
|
+
showBackButton = false,
|
|
16
|
+
backHref = "/",
|
|
17
|
+
showNav = false,
|
|
18
|
+
}: DebuggerHeaderProps) {
|
|
19
|
+
const pathname =
|
|
20
|
+
typeof window !== "undefined" ? window.location.pathname : "/";
|
|
21
|
+
const isSessionsActive = pathname === "/";
|
|
22
|
+
const isTracesActive = pathname === "/traces";
|
|
23
|
+
const [agentName, setAgentName] = useState<string>("Agent");
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
fetch("/api/config")
|
|
27
|
+
.then((res) => res.json())
|
|
28
|
+
.then((data) => {
|
|
29
|
+
if (data.agentName) {
|
|
30
|
+
setAgentName(data.agentName);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {
|
|
34
|
+
// Ignore errors, use default
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ChatHeader.Root className="border-b border-border bg-card h-16">
|
|
40
|
+
<div className="flex items-center gap-3 flex-1">
|
|
41
|
+
{showBackButton && (
|
|
42
|
+
<Button variant="ghost" size="icon" asChild>
|
|
43
|
+
<a href={backHref}>
|
|
44
|
+
<ArrowLeft className="size-4" />
|
|
45
|
+
</a>
|
|
46
|
+
</Button>
|
|
47
|
+
)}
|
|
48
|
+
{showNav ? (
|
|
49
|
+
<div className="flex items-center gap-4">
|
|
50
|
+
<ChatHeader.Title>{agentName}</ChatHeader.Title>
|
|
51
|
+
<nav className="flex items-center gap-1">
|
|
52
|
+
<a
|
|
53
|
+
href="/"
|
|
54
|
+
className={cn(
|
|
55
|
+
"px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
|
56
|
+
isSessionsActive
|
|
57
|
+
? "bg-muted text-foreground"
|
|
58
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
Sessions
|
|
62
|
+
</a>
|
|
63
|
+
<a
|
|
64
|
+
href="/traces"
|
|
65
|
+
className={cn(
|
|
66
|
+
"px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
|
67
|
+
isTracesActive
|
|
68
|
+
? "bg-muted text-foreground"
|
|
69
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted/50",
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
Traces
|
|
73
|
+
</a>
|
|
74
|
+
</nav>
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<ChatHeader.Title>{title}</ChatHeader.Title>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<ChatHeader.Actions>
|
|
81
|
+
<ThemeToggle />
|
|
82
|
+
</ChatHeader.Actions>
|
|
83
|
+
</ChatHeader.Root>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { DebuggerHeader } from "./DebuggerHeader";
|
|
3
|
+
|
|
4
|
+
interface DebuggerLayoutProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
title: string;
|
|
7
|
+
showBackButton?: boolean;
|
|
8
|
+
backHref?: string;
|
|
9
|
+
showNav?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DebuggerLayout({
|
|
13
|
+
children,
|
|
14
|
+
title,
|
|
15
|
+
showBackButton = false,
|
|
16
|
+
backHref = "/",
|
|
17
|
+
showNav = false,
|
|
18
|
+
}: DebuggerLayoutProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="h-screen flex flex-col">
|
|
21
|
+
<DebuggerHeader
|
|
22
|
+
title={title}
|
|
23
|
+
showBackButton={showBackButton}
|
|
24
|
+
backHref={backHref}
|
|
25
|
+
showNav={showNav}
|
|
26
|
+
/>
|
|
27
|
+
<div className="flex-1 overflow-hidden">{children}</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -48,7 +48,7 @@ function LogRow({ log }: { log: Log }) {
|
|
|
48
48
|
return (
|
|
49
49
|
<div>
|
|
50
50
|
<div
|
|
51
|
-
className={`flex items-start gap-2 py-1 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
|
|
51
|
+
className={`flex items-start gap-2 py-1.5 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
|
|
52
52
|
onClick={() => hasDetails && setExpanded(!expanded)}
|
|
53
53
|
>
|
|
54
54
|
<span
|
|
@@ -93,7 +93,7 @@ export function LogList({ logs }: LogListProps) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
|
-
<div className="font-mono text-sm space-y-0
|
|
96
|
+
<div className="font-mono text-sm space-y-0">
|
|
97
97
|
{logs.map((log) => (
|
|
98
98
|
<LogRow key={log.id} log={log} />
|
|
99
99
|
))}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
interface MessageBubbleProps {
|
|
4
|
+
content: string | null;
|
|
5
|
+
type: "user" | "agent";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function MessageBubble({ content, type }: MessageBubbleProps) {
|
|
9
|
+
const displayContent =
|
|
10
|
+
content || (type === "user" ? "No user input" : "No response");
|
|
11
|
+
const isPlaceholder = !content;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cn(
|
|
16
|
+
"max-w-[75%] rounded-2xl px-4 py-2 transition-all",
|
|
17
|
+
"line-clamp-4 whitespace-pre-wrap break-words text-sm",
|
|
18
|
+
type === "user"
|
|
19
|
+
? "bg-primary text-primary-foreground"
|
|
20
|
+
: "bg-muted text-foreground",
|
|
21
|
+
isPlaceholder && "italic opacity-60",
|
|
22
|
+
)}
|
|
23
|
+
title={content || undefined}
|
|
24
|
+
>
|
|
25
|
+
{displayContent}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ConversationTrace } from "../types";
|
|
4
|
+
import { MessageBubble } from "./MessageBubble";
|
|
4
5
|
|
|
5
6
|
function formatRelativeTime(nanoseconds: number): string {
|
|
6
7
|
const ms = nanoseconds / 1_000_000;
|
|
@@ -13,17 +14,11 @@ function formatRelativeTime(nanoseconds: number): string {
|
|
|
13
14
|
return new Date(ms).toLocaleDateString();
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function formatDuration(startNano: number, endNano: number): string {
|
|
17
|
-
const ms = (endNano - startNano) / 1_000_000;
|
|
18
|
-
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
19
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
interface SessionTraceListProps {
|
|
23
18
|
sessionId: string;
|
|
24
19
|
selectedTraceId: string | null;
|
|
25
20
|
onSelectTrace: (traceId: string) => void;
|
|
26
|
-
onTracesLoaded?: (traces:
|
|
21
|
+
onTracesLoaded?: (traces: { trace_id: string }[]) => void;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
export function SessionTraceList({
|
|
@@ -32,23 +27,25 @@ export function SessionTraceList({
|
|
|
32
27
|
onSelectTrace,
|
|
33
28
|
onTracesLoaded,
|
|
34
29
|
}: SessionTraceListProps) {
|
|
35
|
-
const [traces, setTraces] = useState<
|
|
30
|
+
const [traces, setTraces] = useState<ConversationTrace[]>([]);
|
|
36
31
|
const [loading, setLoading] = useState(true);
|
|
37
32
|
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
38
34
|
|
|
39
35
|
const fetchTraces = useCallback(() => {
|
|
40
36
|
setLoading(true);
|
|
41
37
|
const params = new URLSearchParams();
|
|
42
38
|
params.set("sessionId", sessionId);
|
|
43
|
-
fetch(`/api/
|
|
39
|
+
fetch(`/api/session-conversation?${params}`)
|
|
44
40
|
.then((res) => {
|
|
45
|
-
if (!res.ok) throw new Error("Failed to fetch
|
|
41
|
+
if (!res.ok) throw new Error("Failed to fetch conversation");
|
|
46
42
|
return res.json();
|
|
47
43
|
})
|
|
48
|
-
.then((data:
|
|
44
|
+
.then((data: ConversationTrace[]) => {
|
|
49
45
|
setTraces(data);
|
|
50
46
|
setLoading(false);
|
|
51
|
-
onTracesLoaded
|
|
47
|
+
// Convert to format expected by onTracesLoaded callback
|
|
48
|
+
onTracesLoaded?.(data.map((t) => ({ trace_id: t.trace_id })));
|
|
52
49
|
})
|
|
53
50
|
.catch((err) => {
|
|
54
51
|
setError(err.message);
|
|
@@ -60,9 +57,18 @@ export function SessionTraceList({
|
|
|
60
57
|
fetchTraces();
|
|
61
58
|
}, [fetchTraces]);
|
|
62
59
|
|
|
60
|
+
// Auto-scroll to bottom when traces load
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!loading && traces.length > 0 && scrollRef.current) {
|
|
63
|
+
scrollRef.current.scrollIntoView({ behavior: "smooth" });
|
|
64
|
+
}
|
|
65
|
+
}, [loading, traces.length]);
|
|
66
|
+
|
|
63
67
|
if (loading) {
|
|
64
68
|
return (
|
|
65
|
-
<div className="p-4 text-muted-foreground text-sm">
|
|
69
|
+
<div className="p-4 text-muted-foreground text-sm">
|
|
70
|
+
Loading conversation...
|
|
71
|
+
</div>
|
|
66
72
|
);
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -73,44 +79,43 @@ export function SessionTraceList({
|
|
|
73
79
|
if (traces.length === 0) {
|
|
74
80
|
return (
|
|
75
81
|
<div className="p-4 text-muted-foreground text-sm">
|
|
76
|
-
No
|
|
82
|
+
No messages in this session
|
|
77
83
|
</div>
|
|
78
84
|
);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
return (
|
|
82
|
-
<div className="
|
|
88
|
+
<div className="pl-16 pr-6 py-4 space-y-6">
|
|
83
89
|
{traces.map((trace) => {
|
|
84
90
|
const isSelected = trace.trace_id === selectedTraceId;
|
|
85
91
|
return (
|
|
86
92
|
<div
|
|
87
93
|
key={trace.trace_id}
|
|
88
94
|
className={cn(
|
|
89
|
-
"
|
|
90
|
-
isSelected && "bg-
|
|
95
|
+
"space-y-2 cursor-pointer p-3 rounded-lg transition-all",
|
|
96
|
+
isSelected && "bg-blue-500/10 border-2 border-blue-500/30",
|
|
91
97
|
)}
|
|
92
98
|
onClick={() => onSelectTrace(trace.trace_id)}
|
|
93
99
|
>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</span>
|
|
98
|
-
<span className="w-2 h-2 rounded-full bg-green-500 shrink-0" />
|
|
100
|
+
{/* User message - left aligned */}
|
|
101
|
+
<div className="flex justify-start">
|
|
102
|
+
<MessageBubble type="user" content={trace.userInput} />
|
|
99
103
|
</div>
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
|
|
105
|
+
{/* Agent message - right aligned */}
|
|
106
|
+
<div className="flex justify-end">
|
|
107
|
+
<MessageBubble type="agent" content={trace.llmOutput} />
|
|
102
108
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)}
|
|
108
|
-
{" · "}
|
|
109
|
-
{trace.span_count} spans
|
|
109
|
+
|
|
110
|
+
{/* Timestamp - centered */}
|
|
111
|
+
<div className="text-center text-xs text-muted-foreground">
|
|
112
|
+
{formatRelativeTime(trace.start_time_unix_nano)}
|
|
110
113
|
</div>
|
|
111
114
|
</div>
|
|
112
115
|
);
|
|
113
116
|
})}
|
|
117
|
+
{/* Auto-scroll target */}
|
|
118
|
+
<div ref={scrollRef} />
|
|
114
119
|
</div>
|
|
115
120
|
);
|
|
116
121
|
}
|