@townco/debugger 0.1.66 → 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 +3 -3
- package/src/App.tsx +9 -2
- package/src/components/ConversationPanel.tsx +392 -0
- package/src/components/DebuggerHeader.tsx +1 -1
- package/src/components/DiagnosticDetailsPanel.tsx +1095 -0
- package/src/components/DiagnosticPanel.tsx +111 -0
- package/src/components/NavBar.tsx +130 -0
- package/src/components/SpanDetailsPanel.tsx +2 -2
- package/src/components/SpanIcon.tsx +75 -24
- package/src/components/SpanTimeline.tsx +2 -3
- package/src/components/SpansLogsList.tsx +477 -0
- package/src/components/UnifiedTimeline.tsx +1 -2
- package/src/components/ViewControlBar.tsx +79 -0
- package/src/lib/segmentation.ts +391 -0
- package/src/lib/toolRegistry.ts +424 -0
- package/src/pages/SessionLogsView.tsx +585 -0
- package/src/pages/SessionView.tsx +24 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/debugger",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
28
|
-
"@townco/tsconfig": "0.1.
|
|
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 <
|
|
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>
|