@townco/debugger 0.1.67 → 0.1.68

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.67",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3.0"
@@ -24,8 +24,8 @@
24
24
  "@radix-ui/react-select": "^2.2.6",
25
25
  "@radix-ui/react-slot": "^1.2.4",
26
26
  "@radix-ui/react-tabs": "^1.1.13",
27
- "@townco/otlp-server": "0.1.66",
28
- "@townco/tsconfig": "0.1.108",
27
+ "@townco/otlp-server": "0.1.67",
28
+ "@townco/tsconfig": "0.1.109",
29
29
  "@townco/ui": "^0.1.77",
30
30
  "bun-plugin-tailwind": "^0.1.2",
31
31
  "class-variance-authority": "^0.7.1",
package/src/App.tsx CHANGED
@@ -4,6 +4,7 @@ import "./index.css";
4
4
  import { ComparisonView } from "./pages/ComparisonView";
5
5
  import { FindSessions } from "./pages/FindSessions";
6
6
  import { SessionList } from "./pages/SessionList";
7
+ import { SessionLogsView } from "./pages/SessionLogsView";
7
8
  import { SessionView } from "./pages/SessionView";
8
9
  import { TownHall } from "./pages/TownHall";
9
10
  import { TraceDetail } from "./pages/TraceDetail";
@@ -82,10 +83,16 @@ class ErrorBoundary extends Component<
82
83
  function AppContent() {
83
84
  const pathname = window.location.pathname;
84
85
 
85
- // Route: /sessions/:sessionId
86
+ // Route: /sessions/:sessionId/timeline (timeline view)
87
+ const sessionTimelineMatch = pathname.match(/^\/sessions\/(.+)\/timeline$/);
88
+ if (sessionTimelineMatch?.[1]) {
89
+ return <SessionView sessionId={sessionTimelineMatch[1]} />;
90
+ }
91
+
92
+ // Route: /sessions/:sessionId (logs view - default)
86
93
  const sessionMatch = pathname.match(/^\/sessions\/(.+)$/);
87
94
  if (sessionMatch?.[1]) {
88
- return <SessionView sessionId={sessionMatch[1]} />;
95
+ return <SessionLogsView sessionId={sessionMatch[1]} />;
89
96
  }
90
97
 
91
98
  // Route: /trace/:traceId
@@ -0,0 +1,392 @@
1
+ import { useMemo, useRef } from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import type { ConversationItem, Segment } from "../lib/segmentation";
4
+ import { formatTimestamp } from "../lib/segmentation";
5
+ import { getToolIcon } from "../lib/toolRegistry";
6
+
7
+ /**
8
+ * Groups conversation items by subagent context.
9
+ * Items from the same subagent (same subagentSpanId) are grouped together,
10
+ * breaking chronological order to improve developer ergonomics.
11
+ */
12
+ interface ItemGroup {
13
+ subagentId: string | null;
14
+ subagentSpanId: string | null;
15
+ items: ConversationItem[];
16
+ /** Earliest timestamp in the group (for sorting groups) */
17
+ firstTimestamp: number;
18
+ }
19
+
20
+ function groupItemsBySubagent(items: ConversationItem[]): ItemGroup[] {
21
+ // First, separate items into subagent and non-subagent
22
+ const subagentGroups = new Map<string, ConversationItem[]>();
23
+ const nonSubagentItems: ConversationItem[] = [];
24
+
25
+ for (const item of items) {
26
+ if (item.subagentSpanId) {
27
+ const existing = subagentGroups.get(item.subagentSpanId) || [];
28
+ existing.push(item);
29
+ subagentGroups.set(item.subagentSpanId, existing);
30
+ } else {
31
+ nonSubagentItems.push(item);
32
+ }
33
+ }
34
+
35
+ // Build result groups
36
+ const groups: ItemGroup[] = [];
37
+
38
+ // Add non-subagent items as individual "groups" (they stay in order)
39
+ for (const item of nonSubagentItems) {
40
+ groups.push({
41
+ subagentId: null,
42
+ subagentSpanId: null,
43
+ items: [item],
44
+ firstTimestamp: item.timestamp,
45
+ });
46
+ }
47
+
48
+ // Add subagent groups
49
+ for (const [spanId, subagentItems] of subagentGroups) {
50
+ // Sort items within the group by timestamp
51
+ subagentItems.sort((a, b) => a.timestamp - b.timestamp);
52
+ const firstItem = subagentItems[0];
53
+ groups.push({
54
+ subagentId: firstItem?.subagentId ?? null,
55
+ subagentSpanId: spanId,
56
+ items: subagentItems,
57
+ firstTimestamp: firstItem?.timestamp ?? 0,
58
+ });
59
+ }
60
+
61
+ // Sort groups by their first timestamp
62
+ groups.sort((a, b) => a.firstTimestamp - b.firstTimestamp);
63
+
64
+ return groups;
65
+ }
66
+
67
+ interface ConversationPanelProps {
68
+ segments: Segment[];
69
+ highlightedItemId: string | null;
70
+ onItemHover: (itemId: string | null) => void;
71
+ /** The currently selected conversation item (for span highlighting) */
72
+ selectedItemId: string | null;
73
+ /** Called when user clicks on an agent message or tool call */
74
+ onItemSelect: (itemId: string | null, spanId: string | null) => void;
75
+ }
76
+
77
+ interface ToolCallProps {
78
+ item: ConversationItem;
79
+ isSelected: boolean;
80
+ }
81
+
82
+ function ToolCall({ item, isSelected }: ToolCallProps) {
83
+ // Format the input for display
84
+ const formattedInput =
85
+ item.toolInput !== undefined && item.toolInput !== null
86
+ ? typeof item.toolInput === "string"
87
+ ? item.toolInput
88
+ : JSON.stringify(item.toolInput, null, 2)
89
+ : null;
90
+
91
+ // Get the appropriate icon for this tool
92
+ const IconComponent = getToolIcon(item.toolName || "");
93
+
94
+ return (
95
+ <div className="flex flex-col gap-2 items-end">
96
+ {/* Tool Header */}
97
+ <div className="flex items-center gap-1 w-full py-1">
98
+ <IconComponent
99
+ className={cn(
100
+ "size-3.5 shrink-0",
101
+ isSelected ? "text-blue-700" : "text-muted-foreground",
102
+ )}
103
+ />
104
+ <span
105
+ className={cn(
106
+ "text-xs font-medium",
107
+ isSelected ? "text-blue-700" : "text-muted-foreground",
108
+ )}
109
+ >
110
+ {item.toolName}
111
+ </span>
112
+ </div>
113
+
114
+ {/* Tool Content */}
115
+ {formattedInput && (
116
+ <div className="w-full border border-border rounded-lg overflow-hidden">
117
+ {/* Header */}
118
+ <div className="bg-neutral-100 p-2">
119
+ <p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wider">
120
+ Input
121
+ </p>
122
+ </div>
123
+ {/* Body */}
124
+ <div className="bg-background p-2 max-h-[120px] overflow-y-auto">
125
+ <pre className="text-xs font-mono text-foreground whitespace-pre-wrap break-words">
126
+ {formattedInput}
127
+ </pre>
128
+ </div>
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ interface ConversationItemRowProps {
136
+ item: ConversationItem;
137
+ isHighlighted: boolean;
138
+ isSelected: boolean;
139
+ onHover: (itemId: string | null) => void;
140
+ onSelect: (itemId: string, spanId: string | null) => void;
141
+ }
142
+
143
+ function ConversationItemRow({
144
+ item,
145
+ isHighlighted,
146
+ isSelected,
147
+ onHover,
148
+ onSelect,
149
+ }: ConversationItemRowProps) {
150
+ const timestamp = formatTimestamp(item.timestamp);
151
+ const isClickable = item.type === "agent" || item.type === "tool_call";
152
+
153
+ const handleClick = () => {
154
+ if (!isClickable) return;
155
+ // Toggle selection - if already selected, deselect
156
+ if (isSelected) {
157
+ onSelect(item.id, null);
158
+ } else {
159
+ onSelect(item.id, item.spanId ?? null);
160
+ }
161
+ };
162
+
163
+ const interactiveProps = isClickable
164
+ ? {
165
+ onMouseEnter: () => onHover(item.id),
166
+ onMouseLeave: () => onHover(null),
167
+ onClick: handleClick,
168
+ onKeyDown: (e: React.KeyboardEvent) => {
169
+ if (e.key === "Enter" || e.key === " ") {
170
+ e.preventDefault();
171
+ handleClick();
172
+ }
173
+ },
174
+ role: "button" as const,
175
+ tabIndex: 0,
176
+ }
177
+ : {};
178
+
179
+ return (
180
+ <div
181
+ className={cn(
182
+ "flex gap-4 items-start px-6 py-4 transition-colors border-l-2 border-transparent",
183
+ isClickable && "cursor-pointer",
184
+ isClickable && isHighlighted && "bg-neutral-50",
185
+ isSelected && "bg-blue-50 border-blue-700",
186
+ )}
187
+ {...interactiveProps}
188
+ >
189
+ {/* Timestamp column */}
190
+ <div className="w-24 shrink-0">
191
+ <span className="text-xs text-muted-foreground font-mono leading-relaxed tracking-wide">
192
+ {timestamp}
193
+ </span>
194
+ </div>
195
+
196
+ {/* Content column */}
197
+ <div className="flex-1 min-w-0">
198
+ {item.type === "user" ? (
199
+ <div className="flex flex-col gap-1">
200
+ <span className="text-xs font-medium text-muted-foreground py-1">
201
+ User
202
+ </span>
203
+ <div className="bg-neutral-200 rounded-lg px-3 py-2 w-full">
204
+ <p className="text-sm text-foreground leading-relaxed tracking-tight">
205
+ {item.content}
206
+ </p>
207
+ </div>
208
+ </div>
209
+ ) : item.type === "agent" ? (
210
+ <div className="flex flex-col gap-1">
211
+ <span
212
+ className={cn(
213
+ "text-xs font-medium py-1",
214
+ isSelected ? "text-blue-700" : "text-muted-foreground",
215
+ )}
216
+ >
217
+ {item.label || "Agent"}
218
+ </span>
219
+ <p className="text-sm text-foreground leading-relaxed tracking-tight">
220
+ {item.content}
221
+ </p>
222
+ </div>
223
+ ) : item.type === "thinking" ? (
224
+ <div className="flex flex-col gap-1">
225
+ <span className="text-xs font-medium text-muted-foreground py-1">
226
+ {item.label || "Agent"}
227
+ </span>
228
+ <p className="text-sm text-neutral-600 leading-relaxed tracking-tight">
229
+ {item.content}
230
+ </p>
231
+ </div>
232
+ ) : item.type === "tool_call" ? (
233
+ <ToolCall item={item} isSelected={isSelected} />
234
+ ) : null}
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ interface SegmentContentProps {
241
+ segment: Segment;
242
+ highlightedItemId: string | null;
243
+ selectedItemId: string | null;
244
+ onItemHover: (itemId: string | null) => void;
245
+ onItemSelect: (itemId: string | null, spanId: string | null) => void;
246
+ }
247
+
248
+ function SegmentContent({
249
+ segment,
250
+ highlightedItemId,
251
+ selectedItemId,
252
+ onItemHover,
253
+ onItemSelect,
254
+ }: SegmentContentProps) {
255
+ // Group items by subagent
256
+ const groups = useMemo(() => {
257
+ const result = groupItemsBySubagent(segment.conversationItems);
258
+
259
+ // Debug: Log grouping results
260
+ console.log(
261
+ "[DEBUG] Conversation groups for segment:",
262
+ segment.id.slice(-4),
263
+ {
264
+ totalItems: segment.conversationItems.length,
265
+ totalGroups: result.length,
266
+ subagentGroups: result.filter((g) => g.subagentSpanId).length,
267
+ groups: result.map((g) => ({
268
+ subagentId: g.subagentId,
269
+ itemCount: g.items.length,
270
+ itemTypes: g.items.map((i) => i.type),
271
+ })),
272
+ },
273
+ );
274
+
275
+ return result;
276
+ }, [segment.conversationItems, segment.id]);
277
+
278
+ return (
279
+ <div>
280
+ {groups.map((group) => {
281
+ // If this is a subagent group, render with header
282
+ if (group.subagentSpanId) {
283
+ return (
284
+ <div key={group.subagentSpanId} className="flex flex-col">
285
+ {/* Subagent header - sticky */}
286
+ <div className="sticky top-0 z-10 flex items-center gap-2 px-6 py-3 border-b border-border bg-background">
287
+ <span className="text-xs font-medium text-foreground">
288
+ Subagent.{group.subagentId}
289
+ </span>
290
+ </div>
291
+
292
+ {/* Subagent items */}
293
+ {group.items.map((item) => (
294
+ <ConversationItemRow
295
+ key={item.id}
296
+ item={item}
297
+ isHighlighted={highlightedItemId === item.id}
298
+ isSelected={selectedItemId === item.id}
299
+ onHover={onItemHover}
300
+ onSelect={onItemSelect}
301
+ />
302
+ ))}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ // Regular items (no subagent context)
308
+ return group.items.map((item) => (
309
+ <ConversationItemRow
310
+ key={item.id}
311
+ item={item}
312
+ isHighlighted={highlightedItemId === item.id}
313
+ isSelected={selectedItemId === item.id}
314
+ onHover={onItemHover}
315
+ onSelect={onItemSelect}
316
+ />
317
+ ));
318
+ })}
319
+ </div>
320
+ );
321
+ }
322
+
323
+ export function ConversationPanel({
324
+ segments,
325
+ highlightedItemId,
326
+ onItemHover,
327
+ selectedItemId,
328
+ onItemSelect,
329
+ }: ConversationPanelProps) {
330
+ const containerRef = useRef<HTMLDivElement>(null);
331
+
332
+ if (segments.length === 0) {
333
+ return (
334
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
335
+ No conversation data
336
+ </div>
337
+ );
338
+ }
339
+
340
+ return (
341
+ <div
342
+ ref={containerRef}
343
+ className="flex-1"
344
+ style={{ scrollBehavior: "smooth" }}
345
+ >
346
+ <div className="flex flex-col">
347
+ {segments.map((segment, index) => (
348
+ <div key={segment.id}>
349
+ {/* Segment content */}
350
+ <SegmentContent
351
+ segment={segment}
352
+ highlightedItemId={highlightedItemId}
353
+ selectedItemId={selectedItemId}
354
+ onItemHover={onItemHover}
355
+ onItemSelect={onItemSelect}
356
+ />
357
+
358
+ {/* Segment divider */}
359
+ {index < segments.length - 1 && (
360
+ <div className="py-3">
361
+ <div className="h-px bg-border" />
362
+ </div>
363
+ )}
364
+ </div>
365
+ ))}
366
+ </div>
367
+ </div>
368
+ );
369
+ }
370
+
371
+ /**
372
+ * Scrolls the conversation panel to the top
373
+ */
374
+ export function scrollConversationToTop(
375
+ containerRef: React.RefObject<HTMLDivElement | null>,
376
+ ) {
377
+ containerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
378
+ }
379
+
380
+ /**
381
+ * Scrolls the conversation panel to the bottom
382
+ */
383
+ export function scrollConversationToBottom(
384
+ containerRef: React.RefObject<HTMLDivElement | null>,
385
+ ) {
386
+ if (containerRef.current) {
387
+ containerRef.current.scrollTo({
388
+ top: containerRef.current.scrollHeight,
389
+ behavior: "smooth",
390
+ });
391
+ }
392
+ }
@@ -72,7 +72,7 @@ export function DebuggerHeader({
72
72
  };
73
73
 
74
74
  return (
75
- <ChatHeader.Root className="border-b border-border bg-card h-16">
75
+ <ChatHeader.Root className="border-b border-border bg-card h-16 px-8">
76
76
  <div className="flex items-center gap-3 flex-1">
77
77
  {showBackButton && (
78
78
  <Button variant="ghost" size="icon" asChild>