@townco/debugger 0.1.67 → 0.1.69
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
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import {
|
|
4
|
+
formatDuration,
|
|
5
|
+
getSpanDurationMs,
|
|
6
|
+
getSpanTokens,
|
|
7
|
+
type Segment,
|
|
8
|
+
} from "../lib/segmentation";
|
|
9
|
+
import { detectSpanType } from "../lib/spanTypeDetector";
|
|
10
|
+
import type { Log, Span, SpanNode } from "../types";
|
|
11
|
+
import { SpanIcon } from "./SpanIcon";
|
|
12
|
+
|
|
13
|
+
interface SpansLogsListProps {
|
|
14
|
+
segments: Segment[];
|
|
15
|
+
viewMode: "spans" | "logs";
|
|
16
|
+
highlightedSpanId: string | null;
|
|
17
|
+
/** Set of span IDs that should be highlighted (from selected conversation item) */
|
|
18
|
+
selectedSpanIds: Set<string>;
|
|
19
|
+
/** Set of log IDs that should be highlighted (from selected conversation item) */
|
|
20
|
+
selectedLogIds: Set<number>;
|
|
21
|
+
onSpanHover: (spanId: string | null) => void;
|
|
22
|
+
onSpanClick: (span: SpanNode) => void;
|
|
23
|
+
onLogClick: (log: Log) => void;
|
|
24
|
+
/** Ref to scroll to a specific span */
|
|
25
|
+
scrollToSpanId: string | null;
|
|
26
|
+
/** Ref to scroll to a specific log */
|
|
27
|
+
scrollToLogId: number | null;
|
|
28
|
+
/** The currently selected item ID (span_id or log id) for highlighting */
|
|
29
|
+
selectedItemId: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseAttributes(attrs: string | null): Record<string, unknown> {
|
|
33
|
+
if (!attrs) return {};
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(attrs);
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a short identifier from a span_id (last 4 characters)
|
|
43
|
+
*/
|
|
44
|
+
function getShortId(spanId: string): string {
|
|
45
|
+
return spanId.slice(-4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a span is a subagent (Task) span
|
|
50
|
+
*/
|
|
51
|
+
function isSubagentSpan(span: Span): boolean {
|
|
52
|
+
if (span.name !== "agent.tool_call") return false;
|
|
53
|
+
const attrs = parseAttributes(span.attributes);
|
|
54
|
+
const toolName = attrs["tool.name"];
|
|
55
|
+
|
|
56
|
+
if (typeof toolName !== "string") return false;
|
|
57
|
+
|
|
58
|
+
// Check for Task or any tool name containing "subagent" (case-insensitive)
|
|
59
|
+
const isSubagent =
|
|
60
|
+
toolName === "Task" || toolName.toLowerCase().includes("subagent");
|
|
61
|
+
|
|
62
|
+
return isSubagent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SubagentInfo {
|
|
66
|
+
spanId: string;
|
|
67
|
+
shortId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a map of span_id -> subagent info for all spans that are descendants of a Task span
|
|
72
|
+
*/
|
|
73
|
+
function buildSubagentMap(spans: Span[]): Map<string, SubagentInfo> {
|
|
74
|
+
const subagentMap = new Map<string, SubagentInfo>();
|
|
75
|
+
|
|
76
|
+
// Find all Task (subagent) spans
|
|
77
|
+
const taskSpans = spans.filter(isSubagentSpan);
|
|
78
|
+
|
|
79
|
+
// For each Task span, mark it and all its descendants
|
|
80
|
+
for (const taskSpan of taskSpans) {
|
|
81
|
+
const subagentInfo: SubagentInfo = {
|
|
82
|
+
spanId: taskSpan.span_id,
|
|
83
|
+
shortId: getShortId(taskSpan.span_id),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Mark the Task span itself
|
|
87
|
+
subagentMap.set(taskSpan.span_id, subagentInfo);
|
|
88
|
+
|
|
89
|
+
// Find all descendants using BFS
|
|
90
|
+
const queue: string[] = [taskSpan.span_id];
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const currentId = queue.shift();
|
|
93
|
+
if (!currentId) break;
|
|
94
|
+
// Find children of current span
|
|
95
|
+
for (const span of spans) {
|
|
96
|
+
if (span.parent_span_id === currentId) {
|
|
97
|
+
subagentMap.set(span.span_id, subagentInfo);
|
|
98
|
+
queue.push(span.span_id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return subagentMap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getSpanDisplayName(span: Span, subagentInfo?: SubagentInfo): string {
|
|
108
|
+
const attrs = parseAttributes(span.attributes);
|
|
109
|
+
const spanType = detectSpanType(span);
|
|
110
|
+
|
|
111
|
+
let baseName = span.name;
|
|
112
|
+
|
|
113
|
+
if (spanType === "subagent") {
|
|
114
|
+
// Task/Subagent span - include the short span ID for identification
|
|
115
|
+
const shortId = getShortId(span.span_id);
|
|
116
|
+
try {
|
|
117
|
+
const toolInput = attrs["tool.input"];
|
|
118
|
+
const input =
|
|
119
|
+
typeof toolInput === "string" ? JSON.parse(toolInput) : toolInput;
|
|
120
|
+
if (input?.agentName) {
|
|
121
|
+
baseName = `Subagent .${shortId} (${input.agentName})`;
|
|
122
|
+
} else {
|
|
123
|
+
baseName = `Subagent .${shortId}`;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
baseName = `Subagent .${shortId}`;
|
|
127
|
+
}
|
|
128
|
+
} else if (spanType === "tool_call") {
|
|
129
|
+
const toolName = attrs["tool.name"] as string;
|
|
130
|
+
baseName = toolName || span.name;
|
|
131
|
+
} else if (spanType === "chat") {
|
|
132
|
+
baseName = (attrs["gen_ai.request.model"] as string) || span.name;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If this span is part of a subagent (but not the Task span itself), prefix with subagent ID
|
|
136
|
+
if (subagentInfo && !isSubagentSpan(span)) {
|
|
137
|
+
return `[.${subagentInfo.shortId}] ${baseName}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return baseName;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build a hierarchical span tree from flat spans
|
|
145
|
+
*/
|
|
146
|
+
function buildSpanTree(spans: Span[]): SpanNode[] {
|
|
147
|
+
const spanMap = new Map<string, SpanNode>();
|
|
148
|
+
const roots: SpanNode[] = [];
|
|
149
|
+
|
|
150
|
+
// First pass: create SpanNode objects
|
|
151
|
+
for (const span of spans) {
|
|
152
|
+
spanMap.set(span.span_id, {
|
|
153
|
+
...span,
|
|
154
|
+
children: [],
|
|
155
|
+
depth: 0,
|
|
156
|
+
durationMs: getSpanDurationMs(span),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Second pass: build the tree
|
|
161
|
+
for (const span of spans) {
|
|
162
|
+
const node = spanMap.get(span.span_id);
|
|
163
|
+
if (!node) continue;
|
|
164
|
+
|
|
165
|
+
if (span.parent_span_id && spanMap.has(span.parent_span_id)) {
|
|
166
|
+
const parent = spanMap.get(span.parent_span_id);
|
|
167
|
+
if (parent) {
|
|
168
|
+
parent.children.push(node);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
roots.push(node);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Third pass: set depths
|
|
176
|
+
const setDepth = (nodes: SpanNode[], depth: number) => {
|
|
177
|
+
for (const node of nodes) {
|
|
178
|
+
node.depth = depth;
|
|
179
|
+
setDepth(node.children, depth + 1);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
setDepth(roots, 0);
|
|
183
|
+
|
|
184
|
+
return roots;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Flatten span tree for rendering
|
|
189
|
+
*/
|
|
190
|
+
function flattenSpanTree(
|
|
191
|
+
nodes: SpanNode[],
|
|
192
|
+
): Array<{ node: SpanNode; depth: number }> {
|
|
193
|
+
const result: Array<{ node: SpanNode; depth: number }> = [];
|
|
194
|
+
for (const node of nodes) {
|
|
195
|
+
result.push({ node, depth: node.depth });
|
|
196
|
+
if (node.children.length > 0) {
|
|
197
|
+
result.push(...flattenSpanTree(node.children));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface SpanRowProps {
|
|
204
|
+
span: SpanNode;
|
|
205
|
+
depth: number;
|
|
206
|
+
isHighlighted: boolean;
|
|
207
|
+
isSelected: boolean;
|
|
208
|
+
onHover: (spanId: string | null) => void;
|
|
209
|
+
onClick: (span: SpanNode) => void;
|
|
210
|
+
spanRef?: (el: HTMLButtonElement | null) => void;
|
|
211
|
+
subagentInfo?: SubagentInfo | undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function SpanRow({
|
|
215
|
+
span,
|
|
216
|
+
depth,
|
|
217
|
+
isHighlighted: _isHighlighted,
|
|
218
|
+
isSelected,
|
|
219
|
+
onHover,
|
|
220
|
+
onClick,
|
|
221
|
+
spanRef,
|
|
222
|
+
subagentInfo,
|
|
223
|
+
}: SpanRowProps) {
|
|
224
|
+
const displayName = getSpanDisplayName(span, subagentInfo);
|
|
225
|
+
const durationMs = getSpanDurationMs(span);
|
|
226
|
+
const tokens = getSpanTokens(span);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<button
|
|
230
|
+
ref={spanRef}
|
|
231
|
+
type="button"
|
|
232
|
+
className={cn(
|
|
233
|
+
"flex items-center gap-2 h-[38px] w-full px-4 py-2 text-left border-b border-border hover:bg-accent",
|
|
234
|
+
isSelected && "bg-blue-50 hover:bg-blue-100/70",
|
|
235
|
+
)}
|
|
236
|
+
style={{ paddingLeft: `${16 + depth * 20}px` }}
|
|
237
|
+
onMouseEnter={() => onHover(span.span_id)}
|
|
238
|
+
onMouseLeave={() => onHover(null)}
|
|
239
|
+
onClick={() => onClick(span)}
|
|
240
|
+
>
|
|
241
|
+
{/* Span icon */}
|
|
242
|
+
<SpanIcon span={span} />
|
|
243
|
+
|
|
244
|
+
{/* Span name */}
|
|
245
|
+
<span className="flex-1 text-sm truncate">{displayName}</span>
|
|
246
|
+
|
|
247
|
+
{/* Duration */}
|
|
248
|
+
<span className="w-[120px] text-xs text-muted-foreground shrink-0">
|
|
249
|
+
{formatDuration(durationMs)}
|
|
250
|
+
</span>
|
|
251
|
+
|
|
252
|
+
{/* Tokens */}
|
|
253
|
+
<span className="w-[120px] text-xs text-muted-foreground shrink-0">
|
|
254
|
+
{tokens > 0 ? tokens.toLocaleString() : "-"}
|
|
255
|
+
</span>
|
|
256
|
+
</button>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface LogRowProps {
|
|
261
|
+
log: Log;
|
|
262
|
+
isHighlighted: boolean;
|
|
263
|
+
isSelected: boolean;
|
|
264
|
+
onClick: (log: Log) => void;
|
|
265
|
+
logRef?: (el: HTMLButtonElement | null) => void;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function LogRow({
|
|
269
|
+
log,
|
|
270
|
+
isHighlighted,
|
|
271
|
+
isSelected,
|
|
272
|
+
onClick,
|
|
273
|
+
logRef,
|
|
274
|
+
}: LogRowProps) {
|
|
275
|
+
const timestamp = new Date(log.timestamp_unix_nano / 1_000_000);
|
|
276
|
+
const timeStr = timestamp.toLocaleTimeString("en-US", {
|
|
277
|
+
hour: "2-digit",
|
|
278
|
+
minute: "2-digit",
|
|
279
|
+
second: "2-digit",
|
|
280
|
+
hour12: false,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const severityColor =
|
|
284
|
+
log.severity_number >= 17
|
|
285
|
+
? "text-red-500" // ERROR+
|
|
286
|
+
: log.severity_number >= 13
|
|
287
|
+
? "text-yellow-500" // WARN
|
|
288
|
+
: "text-muted-foreground"; // INFO and below
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<button
|
|
292
|
+
ref={logRef}
|
|
293
|
+
type="button"
|
|
294
|
+
onClick={() => onClick(log)}
|
|
295
|
+
className={cn(
|
|
296
|
+
"flex items-center gap-2 h-[38px] w-full px-4 py-2 border-b border-border transition-colors text-left hover:bg-muted/50",
|
|
297
|
+
isHighlighted && "bg-accent",
|
|
298
|
+
isSelected && "bg-blue-50",
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
{/* Timestamp */}
|
|
302
|
+
<span className="w-[100px] text-xs text-muted-foreground font-mono shrink-0">
|
|
303
|
+
{timeStr}
|
|
304
|
+
</span>
|
|
305
|
+
|
|
306
|
+
{/* Severity */}
|
|
307
|
+
<span
|
|
308
|
+
className={cn("w-[60px] text-xs font-medium shrink-0", severityColor)}
|
|
309
|
+
>
|
|
310
|
+
{log.severity_text || `LVL${log.severity_number}`}
|
|
311
|
+
</span>
|
|
312
|
+
|
|
313
|
+
{/* Body */}
|
|
314
|
+
<span className="flex-1 text-sm truncate">{log.body || "-"}</span>
|
|
315
|
+
</button>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function SpansLogsList({
|
|
320
|
+
segments,
|
|
321
|
+
viewMode,
|
|
322
|
+
highlightedSpanId,
|
|
323
|
+
selectedSpanIds,
|
|
324
|
+
selectedLogIds,
|
|
325
|
+
onSpanHover,
|
|
326
|
+
onSpanClick,
|
|
327
|
+
onLogClick,
|
|
328
|
+
scrollToSpanId,
|
|
329
|
+
scrollToLogId,
|
|
330
|
+
selectedItemId,
|
|
331
|
+
}: SpansLogsListProps) {
|
|
332
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
333
|
+
const spanRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
334
|
+
const logRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
|
335
|
+
|
|
336
|
+
// Scroll to selected span
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (scrollToSpanId && spanRefs.current.has(scrollToSpanId)) {
|
|
339
|
+
const element = spanRefs.current.get(scrollToSpanId);
|
|
340
|
+
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
341
|
+
}
|
|
342
|
+
}, [scrollToSpanId]);
|
|
343
|
+
|
|
344
|
+
// Scroll to selected log
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
if (scrollToLogId !== null && logRefs.current.has(scrollToLogId)) {
|
|
347
|
+
const element = logRefs.current.get(scrollToLogId);
|
|
348
|
+
element?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
349
|
+
}
|
|
350
|
+
}, [scrollToLogId]);
|
|
351
|
+
|
|
352
|
+
const setSpanRef = (spanId: string, el: HTMLButtonElement | null) => {
|
|
353
|
+
if (el) {
|
|
354
|
+
spanRefs.current.set(spanId, el);
|
|
355
|
+
} else {
|
|
356
|
+
spanRefs.current.delete(spanId);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const setLogRef = (logId: number, el: HTMLButtonElement | null) => {
|
|
361
|
+
if (el) {
|
|
362
|
+
logRefs.current.set(logId, el);
|
|
363
|
+
} else {
|
|
364
|
+
logRefs.current.delete(logId);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Get all spans and logs from all segments
|
|
369
|
+
const getVisibleData = () => {
|
|
370
|
+
const allSpans: Span[] = [];
|
|
371
|
+
const allLogs: Log[] = [];
|
|
372
|
+
for (const segment of segments) {
|
|
373
|
+
allSpans.push(...segment.spans);
|
|
374
|
+
allLogs.push(...segment.logs);
|
|
375
|
+
}
|
|
376
|
+
return { spans: allSpans, logs: allLogs };
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const { spans, logs } = getVisibleData();
|
|
380
|
+
|
|
381
|
+
if (viewMode === "logs") {
|
|
382
|
+
if (logs.length === 0) {
|
|
383
|
+
return (
|
|
384
|
+
<div className="flex-1 flex items-center justify-center text-muted-foreground bg-muted/30">
|
|
385
|
+
No logs for this segment
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<div ref={containerRef} className="flex-1 overflow-y-auto bg-muted/30">
|
|
392
|
+
{/* Header */}
|
|
393
|
+
<div className="flex items-center gap-2 h-[38px] px-4 py-2 border-b border-border bg-muted/50 sticky top-0">
|
|
394
|
+
<span className="w-[100px] text-xs font-medium text-muted-foreground shrink-0">
|
|
395
|
+
Time
|
|
396
|
+
</span>
|
|
397
|
+
<span className="w-[60px] text-xs font-medium text-muted-foreground shrink-0">
|
|
398
|
+
Level
|
|
399
|
+
</span>
|
|
400
|
+
<span className="flex-1 text-xs font-medium text-muted-foreground">
|
|
401
|
+
Message
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{/* Log rows */}
|
|
406
|
+
{logs
|
|
407
|
+
.sort((a, b) => a.timestamp_unix_nano - b.timestamp_unix_nano)
|
|
408
|
+
.map((log) => (
|
|
409
|
+
<LogRow
|
|
410
|
+
key={`${log.trace_id}-${log.id}`}
|
|
411
|
+
log={log}
|
|
412
|
+
isHighlighted={selectedLogIds.has(log.id)}
|
|
413
|
+
isSelected={selectedItemId === String(log.id)}
|
|
414
|
+
onClick={onLogClick}
|
|
415
|
+
logRef={(el) => setLogRef(log.id, el)}
|
|
416
|
+
/>
|
|
417
|
+
))}
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Spans view
|
|
423
|
+
if (spans.length === 0) {
|
|
424
|
+
return (
|
|
425
|
+
<div className="flex-1 flex items-center justify-center text-muted-foreground bg-muted/30">
|
|
426
|
+
No spans for this segment
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div ref={containerRef} className="flex-1 overflow-y-auto bg-muted/30">
|
|
433
|
+
{/* Header */}
|
|
434
|
+
<div className="flex items-center gap-2 h-[38px] px-4 py-2 border-b border-border bg-muted/50 sticky top-0">
|
|
435
|
+
<span className="flex-1 text-xs font-medium text-muted-foreground">
|
|
436
|
+
Process
|
|
437
|
+
</span>
|
|
438
|
+
<span className="w-[120px] text-xs font-medium text-muted-foreground shrink-0">
|
|
439
|
+
Duration
|
|
440
|
+
</span>
|
|
441
|
+
<span className="w-[120px] text-xs font-medium text-muted-foreground shrink-0">
|
|
442
|
+
Tokens
|
|
443
|
+
</span>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
{/* Span rows grouped by segment */}
|
|
447
|
+
{segments.map((segment) => {
|
|
448
|
+
const segmentSpanTree = buildSpanTree(segment.spans);
|
|
449
|
+
const segmentFlatSpans = flattenSpanTree(segmentSpanTree);
|
|
450
|
+
const subagentMap = buildSubagentMap(segment.spans);
|
|
451
|
+
|
|
452
|
+
if (segmentFlatSpans.length === 0) return null;
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div key={segment.id}>
|
|
456
|
+
{segmentFlatSpans.map(({ node, depth }) => (
|
|
457
|
+
<SpanRow
|
|
458
|
+
key={node.span_id}
|
|
459
|
+
span={node}
|
|
460
|
+
depth={depth}
|
|
461
|
+
isHighlighted={highlightedSpanId === node.span_id}
|
|
462
|
+
isSelected={
|
|
463
|
+
selectedSpanIds.has(node.span_id) ||
|
|
464
|
+
selectedItemId === node.span_id
|
|
465
|
+
}
|
|
466
|
+
onHover={onSpanHover}
|
|
467
|
+
onClick={onSpanClick}
|
|
468
|
+
spanRef={(el) => setSpanRef(node.span_id, el)}
|
|
469
|
+
subagentInfo={subagentMap.get(node.span_id)}
|
|
470
|
+
/>
|
|
471
|
+
))}
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
})}
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
@@ -238,7 +238,6 @@ export function UnifiedTimeline({
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
const attrs = parseAttributes(span.attributes);
|
|
241
|
-
const spanType = detectSpanType(span);
|
|
242
241
|
|
|
243
242
|
let displayName = span.name;
|
|
244
243
|
if (span.name === "agent.tool_call") {
|
|
@@ -287,7 +286,7 @@ export function UnifiedTimeline({
|
|
|
287
286
|
}
|
|
288
287
|
}}
|
|
289
288
|
>
|
|
290
|
-
<SpanIcon
|
|
289
|
+
<SpanIcon span={span} className="w-4 h-4 shrink-0" />
|
|
291
290
|
<span className="text-xs truncate">{displayName}</span>
|
|
292
291
|
</div>
|
|
293
292
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp, Settings } from "lucide-react";
|
|
2
|
+
import { Button } from "./ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "./ui/select";
|
|
10
|
+
|
|
11
|
+
export type ViewMode = "spans" | "logs" | "timeline";
|
|
12
|
+
|
|
13
|
+
interface ViewControlBarProps {
|
|
14
|
+
viewMode: ViewMode;
|
|
15
|
+
onViewModeChange: (mode: ViewMode) => void;
|
|
16
|
+
onScrollUp?: () => void;
|
|
17
|
+
onScrollDown?: () => void;
|
|
18
|
+
onSettings?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ViewControlBar({
|
|
22
|
+
viewMode,
|
|
23
|
+
onViewModeChange,
|
|
24
|
+
onScrollUp,
|
|
25
|
+
onScrollDown,
|
|
26
|
+
onSettings,
|
|
27
|
+
}: ViewControlBarProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="h-[60px] border-b border-border bg-muted/50 flex items-center justify-between px-4">
|
|
30
|
+
{/* Left side - View mode dropdown */}
|
|
31
|
+
<div className="flex items-center gap-4">
|
|
32
|
+
<Select
|
|
33
|
+
value={viewMode}
|
|
34
|
+
onValueChange={(value) => onViewModeChange(value as ViewMode)}
|
|
35
|
+
>
|
|
36
|
+
<SelectTrigger className="w-[160px] bg-background">
|
|
37
|
+
<SelectValue placeholder="Select view" />
|
|
38
|
+
</SelectTrigger>
|
|
39
|
+
<SelectContent>
|
|
40
|
+
<SelectItem value="spans">Spans</SelectItem>
|
|
41
|
+
<SelectItem value="logs">Logs</SelectItem>
|
|
42
|
+
<SelectItem value="timeline">Timeline</SelectItem>
|
|
43
|
+
</SelectContent>
|
|
44
|
+
</Select>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Right side - Icon buttons */}
|
|
48
|
+
<div className="flex items-center gap-2">
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
size="icon"
|
|
52
|
+
className="h-9 w-9 shadow-sm"
|
|
53
|
+
onClick={onScrollUp}
|
|
54
|
+
title="Scroll up"
|
|
55
|
+
>
|
|
56
|
+
<ArrowUp className="size-4" />
|
|
57
|
+
</Button>
|
|
58
|
+
<Button
|
|
59
|
+
variant="outline"
|
|
60
|
+
size="icon"
|
|
61
|
+
className="h-9 w-9 shadow-sm"
|
|
62
|
+
onClick={onScrollDown}
|
|
63
|
+
title="Scroll down"
|
|
64
|
+
>
|
|
65
|
+
<ArrowDown className="size-4" />
|
|
66
|
+
</Button>
|
|
67
|
+
<Button
|
|
68
|
+
variant="outline"
|
|
69
|
+
size="icon"
|
|
70
|
+
className="h-9 w-9 shadow-sm"
|
|
71
|
+
onClick={onSettings}
|
|
72
|
+
title="Settings"
|
|
73
|
+
>
|
|
74
|
+
<Settings className="size-4" />
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|