@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.
@@ -0,0 +1,142 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { DebuggerLayout } from "../components/DebuggerLayout";
11
+ import type { Session } from "../types";
12
+
13
+ function formatDuration(startNano: number, endNano: number): string {
14
+ const ms = (endNano - startNano) / 1_000_000;
15
+ if (ms < 1000) return `${ms.toFixed(2)}ms`;
16
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
17
+ return `${(ms / 60000).toFixed(2)}m`;
18
+ }
19
+
20
+ function formatTimestamp(nanoseconds: number): string {
21
+ return new Date(nanoseconds / 1_000_000).toLocaleString();
22
+ }
23
+
24
+ function formatRelativeTime(nanoseconds: number): string {
25
+ const ms = Date.now() - nanoseconds / 1_000_000;
26
+ const seconds = Math.floor(ms / 1000);
27
+ const minutes = Math.floor(seconds / 60);
28
+ const hours = Math.floor(minutes / 60);
29
+ const days = Math.floor(hours / 24);
30
+
31
+ if (days > 0) return `${days}d ago`;
32
+ if (hours > 0) return `${hours}h ago`;
33
+ if (minutes > 0) return `${minutes}m ago`;
34
+ if (seconds > 5) return `${seconds}s ago`;
35
+ return "just now";
36
+ }
37
+
38
+ export function SessionList() {
39
+ const [sessions, setSessions] = useState<Session[]>([]);
40
+ const [loading, setLoading] = useState(true);
41
+ const [error, setError] = useState<string | null>(null);
42
+
43
+ // Fetch sessions function
44
+ const fetchSessions = useCallback(() => {
45
+ setLoading(true);
46
+ fetch("/api/sessions")
47
+ .then((res) => {
48
+ if (!res.ok) throw new Error("Failed to fetch sessions");
49
+ return res.json();
50
+ })
51
+ .then((data) => {
52
+ setSessions(data);
53
+ setLoading(false);
54
+ })
55
+ .catch((err) => {
56
+ setError(err.message);
57
+ setLoading(false);
58
+ });
59
+ }, []);
60
+
61
+ // Fetch sessions on mount
62
+ useEffect(() => {
63
+ fetchSessions();
64
+ }, [fetchSessions]);
65
+
66
+ if (loading) {
67
+ return (
68
+ <DebuggerLayout title="Sessions" showNav>
69
+ <div className="container mx-auto p-8">
70
+ <div className="text-muted-foreground">Loading sessions...</div>
71
+ </div>
72
+ </DebuggerLayout>
73
+ );
74
+ }
75
+
76
+ if (error) {
77
+ return (
78
+ <DebuggerLayout title="Sessions" showNav>
79
+ <div className="container mx-auto p-8">
80
+ <div className="text-red-500">Error: {error}</div>
81
+ </div>
82
+ </DebuggerLayout>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <DebuggerLayout title="Sessions" showNav>
88
+ <div className="container mx-auto p-8">
89
+ <div className="flex gap-2 mb-4">
90
+ <Button variant="outline" onClick={fetchSessions} disabled={loading}>
91
+ Refresh
92
+ </Button>
93
+ </div>
94
+
95
+ {sessions.length === 0 ? (
96
+ <div className="text-muted-foreground">No sessions found</div>
97
+ ) : (
98
+ <div className="space-y-2">
99
+ {sessions.map((session) => (
100
+ <a
101
+ key={session.session_id}
102
+ href={`/sessions/${session.session_id}`}
103
+ className="block"
104
+ >
105
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
106
+ <CardHeader className="py-3">
107
+ <div className="flex items-center justify-between">
108
+ <CardTitle className="text-base font-medium font-mono">
109
+ {session.session_id}
110
+ </CardTitle>
111
+ <span className="text-sm text-muted-foreground">
112
+ {formatRelativeTime(session.last_trace_time)}
113
+ </span>
114
+ </div>
115
+ <CardDescription className="flex items-center gap-4">
116
+ <span>{session.trace_count} traces</span>
117
+ <span>
118
+ Duration:{" "}
119
+ {formatDuration(
120
+ session.first_trace_time,
121
+ session.last_trace_time,
122
+ )}
123
+ </span>
124
+ </CardDescription>
125
+ </CardHeader>
126
+ <CardContent className="py-2 pt-0">
127
+ <div className="text-xs text-muted-foreground">
128
+ Started: {formatTimestamp(session.first_trace_time)}
129
+ </div>
130
+ <div className="text-xs text-muted-foreground">
131
+ Last activity: {formatTimestamp(session.last_trace_time)}
132
+ </div>
133
+ </CardContent>
134
+ </Card>
135
+ </a>
136
+ ))}
137
+ </div>
138
+ )}
139
+ </div>
140
+ </DebuggerLayout>
141
+ );
142
+ }
@@ -1,8 +1,7 @@
1
1
  import { useCallback, useState } from "react";
2
- import { Button } from "@/components/ui/button";
2
+ import { DebuggerLayout } from "../components/DebuggerLayout";
3
3
  import { SessionTraceList } from "../components/SessionTraceList";
4
4
  import { TraceDetailContent } from "../components/TraceDetailContent";
5
- import type { Trace } from "../types";
6
5
 
7
6
  function getInitialTraceId(): string | null {
8
7
  const params = new URLSearchParams(window.location.search);
@@ -31,9 +30,10 @@ export function SessionView({ sessionId }: SessionViewProps) {
31
30
  };
32
31
 
33
32
  const handleTracesLoaded = useCallback(
34
- (traces: Trace[]) => {
33
+ (traces: { trace_id: string }[]) => {
35
34
  // Auto-select most recent trace if none selected yet
36
- const mostRecent = traces[0];
35
+ // Most recent is now last in array due to ASC ordering
36
+ const mostRecent = traces[traces.length - 1];
37
37
  if (!autoSelectDone && mostRecent) {
38
38
  setSelectedTraceId(mostRecent.trace_id);
39
39
  setAutoSelectDone(true);
@@ -48,22 +48,11 @@ export function SessionView({ sessionId }: SessionViewProps) {
48
48
  );
49
49
 
50
50
  return (
51
- <div className="h-screen flex flex-col">
52
- {/* Header */}
53
- <div className="border-b p-4 shrink-0">
54
- <Button variant="outline" asChild>
55
- <a href="/">Back to Traces</a>
56
- </Button>
57
- <h1 className="text-xl font-bold mt-2">Session</h1>
58
- <div className="text-sm text-muted-foreground font-mono truncate">
59
- {sessionId}
60
- </div>
61
- </div>
62
-
51
+ <DebuggerLayout title={`Session: ${sessionId}`} showBackButton backHref="/">
63
52
  {/* Split pane container */}
64
- <div className="flex flex-1 overflow-hidden">
53
+ <div className="flex flex-1 overflow-hidden h-full">
65
54
  {/* Left pane - trace list */}
66
- <div className="w-80 border-r overflow-y-auto shrink-0">
55
+ <div className="w-[480px] border-r overflow-y-auto shrink-0">
67
56
  <SessionTraceList
68
57
  sessionId={sessionId}
69
58
  selectedTraceId={selectedTraceId}
@@ -83,6 +72,6 @@ export function SessionView({ sessionId }: SessionViewProps) {
83
72
  )}
84
73
  </div>
85
74
  </div>
86
- </div>
75
+ </DebuggerLayout>
87
76
  );
88
77
  }
@@ -1,4 +1,4 @@
1
- import { Button } from "@/components/ui/button";
1
+ import { DebuggerLayout } from "../components/DebuggerLayout";
2
2
  import { TraceDetailContent } from "../components/TraceDetailContent";
3
3
 
4
4
  interface TraceDetailProps {
@@ -7,13 +7,10 @@ interface TraceDetailProps {
7
7
 
8
8
  export function TraceDetail({ traceId }: TraceDetailProps) {
9
9
  return (
10
- <div className="container mx-auto">
11
- <div className="p-8 pb-0">
12
- <Button variant="outline" asChild className="mb-4">
13
- <a href="/">Back to Traces</a>
14
- </Button>
10
+ <DebuggerLayout title="Trace Detail" showBackButton backHref="/traces">
11
+ <div className="container mx-auto">
12
+ <TraceDetailContent traceId={traceId} />
15
13
  </div>
16
- <TraceDetailContent traceId={traceId} />
17
- </div>
14
+ </DebuggerLayout>
18
15
  );
19
16
  }
@@ -8,6 +8,7 @@ import {
8
8
  CardTitle,
9
9
  } from "@/components/ui/card";
10
10
  import { Input } from "@/components/ui/input";
11
+ import { DebuggerLayout } from "../components/DebuggerLayout";
11
12
  import type { Trace } from "../types";
12
13
 
13
14
  function formatDuration(startNano: number, endNano: number): string {
@@ -77,77 +78,81 @@ export function TraceList() {
77
78
 
78
79
  if (loading) {
79
80
  return (
80
- <div className="container mx-auto p-8">
81
- <div className="text-muted-foreground">Loading traces...</div>
82
- </div>
81
+ <DebuggerLayout title="Traces" showNav>
82
+ <div className="container mx-auto p-8">
83
+ <div className="text-muted-foreground">Loading traces...</div>
84
+ </div>
85
+ </DebuggerLayout>
83
86
  );
84
87
  }
85
88
 
86
89
  if (error) {
87
90
  return (
88
- <div className="container mx-auto p-8">
89
- <div className="text-red-500">Error: {error}</div>
90
- </div>
91
+ <DebuggerLayout title="Traces" showNav>
92
+ <div className="container mx-auto p-8">
93
+ <div className="text-red-500">Error: {error}</div>
94
+ </div>
95
+ </DebuggerLayout>
91
96
  );
92
97
  }
93
98
 
94
99
  return (
95
- <div className="container mx-auto p-8">
96
- <h1 className="text-2xl font-bold mb-6">Traces</h1>
100
+ <DebuggerLayout title="Traces" showNav>
101
+ <div className="container mx-auto p-8">
102
+ <div className="flex gap-2 mb-4">
103
+ <Input
104
+ placeholder="Filter by session ID..."
105
+ value={sessionId}
106
+ onChange={(e) => setSessionId(e.target.value)}
107
+ className="max-w-sm"
108
+ />
109
+ <Button variant="outline" onClick={fetchTraces} disabled={loading}>
110
+ Refresh
111
+ </Button>
112
+ </div>
97
113
 
98
- <div className="flex gap-2 mb-4">
99
- <Input
100
- placeholder="Filter by session ID..."
101
- value={sessionId}
102
- onChange={(e) => setSessionId(e.target.value)}
103
- className="max-w-sm"
104
- />
105
- <Button variant="outline" onClick={fetchTraces} disabled={loading}>
106
- Refresh
107
- </Button>
114
+ {traces.length === 0 ? (
115
+ <div className="text-muted-foreground">No traces found</div>
116
+ ) : (
117
+ <div className="space-y-2">
118
+ {traces.map((trace) => (
119
+ <a
120
+ key={trace.trace_id}
121
+ href={`/trace/${trace.trace_id}`}
122
+ className="block"
123
+ >
124
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
125
+ <CardHeader className="py-3">
126
+ <div className="flex items-center justify-between">
127
+ <CardTitle className="text-base font-medium">
128
+ {trace.first_span_name || "Unknown"}
129
+ </CardTitle>
130
+ <span className="text-sm text-muted-foreground">
131
+ {formatDuration(
132
+ trace.start_time_unix_nano,
133
+ trace.end_time_unix_nano,
134
+ )}
135
+ </span>
136
+ </div>
137
+ <CardDescription className="flex items-center gap-4">
138
+ <span>{trace.service_name || "Unknown service"}</span>
139
+ <span>{trace.span_count} spans</span>
140
+ </CardDescription>
141
+ </CardHeader>
142
+ <CardContent className="py-2 pt-0">
143
+ <div className="text-xs text-muted-foreground font-mono">
144
+ {trace.trace_id}
145
+ </div>
146
+ <div className="text-xs text-muted-foreground">
147
+ {formatTimestamp(trace.start_time_unix_nano)}
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+ </a>
152
+ ))}
153
+ </div>
154
+ )}
108
155
  </div>
109
-
110
- {traces.length === 0 ? (
111
- <div className="text-muted-foreground">No traces found</div>
112
- ) : (
113
- <div className="space-y-2">
114
- {traces.map((trace) => (
115
- <a
116
- key={trace.trace_id}
117
- href={`/trace/${trace.trace_id}`}
118
- className="block"
119
- >
120
- <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
121
- <CardHeader className="py-3">
122
- <div className="flex items-center justify-between">
123
- <CardTitle className="text-base font-medium">
124
- {trace.first_span_name || "Unknown"}
125
- </CardTitle>
126
- <span className="text-sm text-muted-foreground">
127
- {formatDuration(
128
- trace.start_time_unix_nano,
129
- trace.end_time_unix_nano,
130
- )}
131
- </span>
132
- </div>
133
- <CardDescription className="flex items-center gap-4">
134
- <span>{trace.service_name || "Unknown service"}</span>
135
- <span>{trace.span_count} spans</span>
136
- </CardDescription>
137
- </CardHeader>
138
- <CardContent className="py-2 pt-0">
139
- <div className="text-xs text-muted-foreground font-mono">
140
- {trace.trace_id}
141
- </div>
142
- <div className="text-xs text-muted-foreground">
143
- {formatTimestamp(trace.start_time_unix_nano)}
144
- </div>
145
- </CardContent>
146
- </Card>
147
- </a>
148
- ))}
149
- </div>
150
- )}
151
- </div>
156
+ </DebuggerLayout>
152
157
  );
153
158
  }
package/src/server.ts CHANGED
@@ -2,6 +2,8 @@ import { createOtlpServer } from "@townco/otlp-server/http";
2
2
  import { serve } from "bun";
3
3
  import { DebuggerDb } from "./db";
4
4
  import index from "./index.html";
5
+ import { extractTurnMessages } from "./lib/turnExtractor";
6
+ import type { ConversationTrace } from "./types";
5
7
 
6
8
  export const DEFAULT_DEBUGGER_PORT = 4000;
7
9
  export const DEFAULT_OTLP_PORT = 4318;
@@ -10,6 +12,7 @@ export interface DebuggerServerOptions {
10
12
  port?: number;
11
13
  otlpPort?: number;
12
14
  dbPath: string;
15
+ agentName?: string;
13
16
  }
14
17
 
15
18
  export interface DebuggerServerResult {
@@ -25,6 +28,7 @@ export function startDebuggerServer(
25
28
  port = DEFAULT_DEBUGGER_PORT,
26
29
  otlpPort = DEFAULT_OTLP_PORT,
27
30
  dbPath,
31
+ agentName = "Agent",
28
32
  } = options;
29
33
 
30
34
  // Start OTLP server (initializes database internally)
@@ -41,6 +45,22 @@ export function startDebuggerServer(
41
45
  const server = serve({
42
46
  port,
43
47
  routes: {
48
+ "/api/config": {
49
+ GET() {
50
+ return Response.json({ agentName });
51
+ },
52
+ },
53
+
54
+ "/api/sessions": {
55
+ GET(req) {
56
+ const url = new URL(req.url);
57
+ const limit = Number.parseInt(url.searchParams.get("limit") || "50");
58
+ const offset = Number.parseInt(url.searchParams.get("offset") || "0");
59
+ const sessions = db.listSessions(limit, offset);
60
+ return Response.json(sessions);
61
+ },
62
+ },
63
+
44
64
  "/api/traces": {
45
65
  GET(req) {
46
66
  const url = new URL(req.url);
@@ -59,7 +79,39 @@ export function startDebuggerServer(
59
79
  if (!data.trace) {
60
80
  return Response.json({ error: "Trace not found" }, { status: 404 });
61
81
  }
62
- return Response.json(data);
82
+ // Extract messages on the server side
83
+ const messages = extractTurnMessages(data.spans, data.logs);
84
+ return Response.json({ ...data, messages });
85
+ },
86
+ },
87
+
88
+ "/api/session-conversation": {
89
+ GET(req) {
90
+ const url = new URL(req.url);
91
+ const sessionId = url.searchParams.get("sessionId");
92
+ if (!sessionId) {
93
+ return Response.json(
94
+ { error: "sessionId parameter is required" },
95
+ { status: 400 },
96
+ );
97
+ }
98
+
99
+ // Get all traces for the session (already sorted ASC)
100
+ const traces = db.listTraces(50, 0, sessionId);
101
+
102
+ // Extract messages for each trace
103
+ const conversation: ConversationTrace[] = traces.map((trace) => {
104
+ const data = db.getTraceById(trace.trace_id);
105
+ const messages = extractTurnMessages(data.spans, data.logs);
106
+ return {
107
+ trace_id: trace.trace_id,
108
+ start_time_unix_nano: trace.start_time_unix_nano,
109
+ userInput: messages.userInput,
110
+ llmOutput: messages.llmOutput,
111
+ };
112
+ });
113
+
114
+ return Response.json(conversation);
63
115
  },
64
116
  },
65
117
 
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export interface Trace {
2
2
  trace_id: string;
3
+ session_id: string | null;
3
4
  service_name: string | null;
4
5
  first_span_name: string | null;
5
6
  start_time_unix_nano: number;
@@ -41,8 +42,29 @@ export interface SpanNode extends Span {
41
42
  durationMs: number;
42
43
  }
43
44
 
44
- export interface TraceDetail {
45
+ export interface TraceDetailRaw {
45
46
  trace: Trace | null;
46
47
  spans: Span[];
47
48
  logs: Log[];
48
49
  }
50
+
51
+ export interface TraceDetail extends TraceDetailRaw {
52
+ messages: {
53
+ userInput: string | null;
54
+ llmOutput: string | null;
55
+ };
56
+ }
57
+
58
+ export interface ConversationTrace {
59
+ trace_id: string;
60
+ start_time_unix_nano: number;
61
+ userInput: string | null;
62
+ llmOutput: string | null;
63
+ }
64
+
65
+ export interface Session {
66
+ session_id: string;
67
+ trace_count: number;
68
+ first_trace_time: number;
69
+ last_trace_time: number;
70
+ }