@townco/debugger 0.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/debugger",
3
- "version": "0.1.4",
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
- "@townco/otlp-server": "0.1.4",
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.46",
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
- // Default: Trace list
92
- return <TraceList />;
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
- <AppContent />
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.5">
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 { Trace } from "../types";
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: Trace[]) => void;
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<Trace[]>([]);
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/traces?${params}`)
39
+ fetch(`/api/session-conversation?${params}`)
44
40
  .then((res) => {
45
- if (!res.ok) throw new Error("Failed to fetch traces");
41
+ if (!res.ok) throw new Error("Failed to fetch conversation");
46
42
  return res.json();
47
43
  })
48
- .then((data: Trace[]) => {
44
+ .then((data: ConversationTrace[]) => {
49
45
  setTraces(data);
50
46
  setLoading(false);
51
- onTracesLoaded?.(data);
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">Loading traces...</div>
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 traces found for this session
82
+ No messages in this session
77
83
  </div>
78
84
  );
79
85
  }
80
86
 
81
87
  return (
82
- <div className="divide-y">
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
- "p-3 cursor-pointer hover:bg-muted/50 transition-colors",
90
- isSelected && "bg-muted border-l-2 border-l-primary",
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
- <div className="flex items-center justify-between gap-2">
95
- <span className="font-medium text-sm truncate flex-1">
96
- {trace.first_span_name || "Unknown"}
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
- <div className="text-xs text-muted-foreground mt-1">
101
- {formatRelativeTime(trace.start_time_unix_nano)}
104
+
105
+ {/* Agent message - right aligned */}
106
+ <div className="flex justify-end">
107
+ <MessageBubble type="agent" content={trace.llmOutput} />
102
108
  </div>
103
- <div className="text-xs text-muted-foreground">
104
- {formatDuration(
105
- trace.start_time_unix_nano,
106
- trace.end_time_unix_nano,
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
  }