@townco/debugger 0.1.22 → 0.1.24

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,173 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { Card, CardContent } from "@/components/ui/card";
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4
+ import type { ConversationTrace, Log, Span } from "../types";
5
+ import { LogList } from "./LogList";
6
+ import { UnifiedTimeline } from "./UnifiedTimeline";
7
+
8
+ interface SessionTimelineViewProps {
9
+ sessionId: string;
10
+ }
11
+
12
+ interface TraceData {
13
+ trace: ConversationTrace;
14
+ spans: Span[];
15
+ logs: Log[];
16
+ }
17
+
18
+ export function SessionTimelineView({ sessionId }: SessionTimelineViewProps) {
19
+ const [traces, setTraces] = useState<ConversationTrace[]>([]);
20
+ const [traceData, setTraceData] = useState<Map<string, TraceData>>(new Map());
21
+ const [loading, setLoading] = useState(true);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ // Fetch all traces for the session
25
+ useEffect(() => {
26
+ const params = new URLSearchParams();
27
+ params.set("sessionId", sessionId);
28
+ fetch(`/api/session-conversation?${params}`)
29
+ .then((res) => {
30
+ if (!res.ok) throw new Error("Failed to fetch conversation");
31
+ return res.json();
32
+ })
33
+ .then((data: ConversationTrace[]) => {
34
+ setTraces(data);
35
+ setLoading(false);
36
+ })
37
+ .catch((err) => {
38
+ setError(err.message);
39
+ setLoading(false);
40
+ });
41
+ }, [sessionId]);
42
+
43
+ // Fetch spans and logs for each trace
44
+ useEffect(() => {
45
+ if (traces.length === 0) return;
46
+
47
+ const fetchAllTraceData = async () => {
48
+ const dataMap = new Map<string, TraceData>();
49
+
50
+ await Promise.all(
51
+ traces.map(async (trace) => {
52
+ try {
53
+ const res = await fetch(`/api/traces/${trace.trace_id}`);
54
+ if (!res.ok) return;
55
+ const data = await res.json();
56
+ // Use the detailed trace data with updated messages
57
+ const updatedTrace: ConversationTrace = {
58
+ trace_id: trace.trace_id,
59
+ start_time_unix_nano: trace.start_time_unix_nano,
60
+ userInput: data.messages?.userInput ?? trace.userInput,
61
+ llmOutput: data.messages?.llmOutput ?? trace.llmOutput,
62
+ agentMessages:
63
+ data.messages?.agentMessages ?? trace.agentMessages,
64
+ };
65
+ dataMap.set(trace.trace_id, {
66
+ trace: updatedTrace,
67
+ spans: data.spans || [],
68
+ logs: data.logs || [],
69
+ });
70
+ } catch (err) {
71
+ console.error(`Failed to fetch trace ${trace.trace_id}:`, err);
72
+ }
73
+ }),
74
+ );
75
+
76
+ setTraceData(dataMap);
77
+ };
78
+
79
+ fetchAllTraceData();
80
+ }, [traces]);
81
+
82
+ // Combine all spans and logs
83
+ const allSpans = useMemo(() => {
84
+ const spans: Array<Span & { traceId: string; turnIndex: number }> = [];
85
+ traces.forEach((trace, index) => {
86
+ const data = traceData.get(trace.trace_id);
87
+ if (data) {
88
+ data.spans.forEach((span) => {
89
+ spans.push({
90
+ ...span,
91
+ traceId: trace.trace_id,
92
+ turnIndex: index,
93
+ });
94
+ });
95
+ }
96
+ });
97
+ return spans;
98
+ }, [traces, traceData]);
99
+
100
+ const allLogs = useMemo(() => {
101
+ const logs: Log[] = [];
102
+ traces.forEach((trace) => {
103
+ const data = traceData.get(trace.trace_id);
104
+ if (data) {
105
+ logs.push(...data.logs);
106
+ }
107
+ });
108
+ return logs;
109
+ }, [traces, traceData]);
110
+
111
+ // Calculate global timeline bounds
112
+ const timelineBounds = useMemo(() => {
113
+ if (allSpans.length === 0) return { start: 0, end: 0 };
114
+
115
+ const start = Math.min(...allSpans.map((s) => s.start_time_unix_nano));
116
+ const end = Math.max(...allSpans.map((s) => s.end_time_unix_nano));
117
+
118
+ return { start, end };
119
+ }, [allSpans]);
120
+
121
+ if (loading) {
122
+ return (
123
+ <div className="p-6">
124
+ <div className="text-muted-foreground">Loading session...</div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ if (error) {
130
+ return (
131
+ <div className="p-6">
132
+ <div className="text-red-500">Error: {error}</div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ if (traces.length === 0) {
138
+ return (
139
+ <div className="p-6">
140
+ <div className="text-muted-foreground">No turns in this session</div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <div className="p-6 space-y-6 bg-background min-h-screen">
147
+ <Tabs defaultValue="timeline">
148
+ <TabsList>
149
+ <TabsTrigger value="timeline">Timeline</TabsTrigger>
150
+ <TabsTrigger value="logs">Logs</TabsTrigger>
151
+ </TabsList>
152
+
153
+ <TabsContent value="timeline" className="mt-6">
154
+ <UnifiedTimeline
155
+ spans={allSpans}
156
+ traces={traces}
157
+ traceData={traceData}
158
+ timelineStart={timelineBounds.start}
159
+ timelineEnd={timelineBounds.end}
160
+ />
161
+ </TabsContent>
162
+
163
+ <TabsContent value="logs" className="mt-6">
164
+ <Card>
165
+ <CardContent className="p-4">
166
+ <LogList logs={allLogs} />
167
+ </CardContent>
168
+ </Card>
169
+ </TabsContent>
170
+ </Tabs>
171
+ </div>
172
+ );
173
+ }
@@ -373,7 +373,7 @@ function RenderSpanTreeRow({
373
373
  return (
374
374
  <>
375
375
  <div
376
- className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer"
376
+ className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer transition-colors"
377
377
  style={{ paddingLeft: `${span.depth * 20 + 16}px` }}
378
378
  onClick={() => onSpanClick(span)}
379
379
  >
@@ -422,14 +422,16 @@ function RenderSpanTreeTimeline({
422
422
 
423
423
  return (
424
424
  <>
425
- <div className="h-[38px] border-b border-border relative flex items-center px-4 hover:bg-muted/50 cursor-pointer">
425
+ <div
426
+ className="h-[38px] border-b border-border relative flex items-center px-4 hover:bg-muted/50 cursor-pointer transition-colors"
427
+ onClick={() => onSpanClick(span)}
428
+ >
426
429
  <div
427
- className={`absolute h-[18px] rounded ${barColor} flex items-center px-1`}
430
+ className={`absolute h-[18px] rounded ${barColor} flex items-center px-1 pointer-events-none`}
428
431
  style={{
429
432
  left: `calc(${timeline.leftPercent}% + 16px)`,
430
433
  width: `calc(${Math.max(timeline.widthPercent, 0.5)}% - 32px)`,
431
434
  }}
432
- onClick={() => onSpanClick(span)}
433
435
  >
434
436
  <span className="text-xs text-white whitespace-nowrap">
435
437
  {span.durationMs.toFixed(2)}ms