@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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import type { Segment } from "../lib/segmentation";
|
|
3
|
+
import type { Log, SpanNode } from "../types";
|
|
4
|
+
import { SpansLogsList } from "./SpansLogsList";
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "./ui/select";
|
|
12
|
+
|
|
13
|
+
export type DiagnosticViewMode = "spans" | "logs";
|
|
14
|
+
|
|
15
|
+
interface DiagnosticPanelProps {
|
|
16
|
+
segments: Segment[];
|
|
17
|
+
viewMode: DiagnosticViewMode;
|
|
18
|
+
onViewModeChange: (mode: DiagnosticViewMode) => void;
|
|
19
|
+
highlightedSpanId: string | null;
|
|
20
|
+
selectedSpanIds: Set<string>;
|
|
21
|
+
selectedLogIds: Set<number>;
|
|
22
|
+
onSpanHover: (spanId: string | null) => void;
|
|
23
|
+
onSpanClick: (span: SpanNode) => void;
|
|
24
|
+
onLogClick: (log: Log) => void;
|
|
25
|
+
scrollToSpanId: string | null;
|
|
26
|
+
scrollToLogId: number | null;
|
|
27
|
+
/** The currently selected item (span or log) ID for highlighting in the list */
|
|
28
|
+
selectedItemId: string | null;
|
|
29
|
+
/** Callback for keyboard navigation */
|
|
30
|
+
onNavigate: (direction: "up" | "down") => void;
|
|
31
|
+
/** Callback for keyboard Enter to confirm selection */
|
|
32
|
+
onEnterSelect: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function DiagnosticPanel({
|
|
36
|
+
segments,
|
|
37
|
+
viewMode,
|
|
38
|
+
onViewModeChange,
|
|
39
|
+
highlightedSpanId,
|
|
40
|
+
selectedSpanIds,
|
|
41
|
+
selectedLogIds,
|
|
42
|
+
onSpanHover,
|
|
43
|
+
onSpanClick,
|
|
44
|
+
onLogClick,
|
|
45
|
+
scrollToSpanId,
|
|
46
|
+
scrollToLogId,
|
|
47
|
+
selectedItemId,
|
|
48
|
+
onNavigate,
|
|
49
|
+
onEnterSelect,
|
|
50
|
+
}: DiagnosticPanelProps) {
|
|
51
|
+
const containerRef = useRef<HTMLElement>(null);
|
|
52
|
+
|
|
53
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
54
|
+
if (e.key === "ArrowUp") {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
onNavigate("up");
|
|
57
|
+
} else if (e.key === "ArrowDown") {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
onNavigate("down");
|
|
60
|
+
} else if (e.key === "Enter") {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
onEnterSelect();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section
|
|
68
|
+
ref={containerRef}
|
|
69
|
+
className="flex flex-col h-full border-l border-border focus:outline-none"
|
|
70
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: Section needs keyboard navigation focus for arrow key navigation
|
|
71
|
+
tabIndex={0}
|
|
72
|
+
onKeyDown={handleKeyDown}
|
|
73
|
+
aria-label="Diagnostic panel"
|
|
74
|
+
>
|
|
75
|
+
{/* Header with view mode selector */}
|
|
76
|
+
<div className="h-12 px-4 flex items-center border-b border-border bg-muted/30 shrink-0">
|
|
77
|
+
<Select
|
|
78
|
+
value={viewMode}
|
|
79
|
+
onValueChange={(value) =>
|
|
80
|
+
onViewModeChange(value as DiagnosticViewMode)
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
<SelectTrigger className="w-[120px] h-8 bg-background" size="sm">
|
|
84
|
+
<SelectValue placeholder="Select view" />
|
|
85
|
+
</SelectTrigger>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
<SelectItem value="spans">Spans</SelectItem>
|
|
88
|
+
<SelectItem value="logs">Logs</SelectItem>
|
|
89
|
+
</SelectContent>
|
|
90
|
+
</Select>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Content area */}
|
|
94
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
95
|
+
<SpansLogsList
|
|
96
|
+
segments={segments}
|
|
97
|
+
viewMode={viewMode}
|
|
98
|
+
highlightedSpanId={highlightedSpanId}
|
|
99
|
+
selectedSpanIds={selectedSpanIds}
|
|
100
|
+
selectedLogIds={selectedLogIds}
|
|
101
|
+
onSpanHover={onSpanHover}
|
|
102
|
+
onSpanClick={onSpanClick}
|
|
103
|
+
onLogClick={onLogClick}
|
|
104
|
+
scrollToSpanId={scrollToSpanId}
|
|
105
|
+
scrollToLogId={scrollToLogId}
|
|
106
|
+
selectedItemId={selectedItemId}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Tooltip,
|
|
3
|
+
TooltipContent,
|
|
4
|
+
TooltipProvider,
|
|
5
|
+
TooltipTrigger,
|
|
6
|
+
} from "@townco/ui/gui";
|
|
7
|
+
import { ChevronRight, Home, List, SquareChartGantt } from "lucide-react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { Button } from "./ui/button";
|
|
10
|
+
|
|
11
|
+
interface NavBarProps {
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface BreadcrumbItem {
|
|
17
|
+
label: string;
|
|
18
|
+
href?: string;
|
|
19
|
+
icon?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function NavBar({ sessionId, userId }: NavBarProps) {
|
|
23
|
+
const [agentName, setAgentName] = useState<string>("Agent");
|
|
24
|
+
const pathname =
|
|
25
|
+
typeof window !== "undefined" ? window.location.pathname : "";
|
|
26
|
+
const isTimelineView = pathname.endsWith("/timeline");
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetch("/api/config")
|
|
30
|
+
.then((res) => res.json())
|
|
31
|
+
.then((data) => {
|
|
32
|
+
if (data.agentName) {
|
|
33
|
+
setAgentName(data.agentName);
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {
|
|
37
|
+
// Ignore errors, use default
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const breadcrumbs: BreadcrumbItem[] = [
|
|
42
|
+
{
|
|
43
|
+
label: "",
|
|
44
|
+
href: "/",
|
|
45
|
+
icon: <Home className="size-4" />,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: agentName,
|
|
49
|
+
href: "/",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
if (userId) {
|
|
54
|
+
breadcrumbs.push({
|
|
55
|
+
label: `[${userId}]`,
|
|
56
|
+
href: `/sessions?userId=${userId}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (sessionId) {
|
|
61
|
+
breadcrumbs.push({
|
|
62
|
+
label: `${sessionId}`,
|
|
63
|
+
href: `/sessions/${sessionId}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<header className="h-16 border-b border-border bg-muted/50 flex items-center justify-between px-6">
|
|
69
|
+
<nav className="flex items-center gap-2" aria-label="Breadcrumb">
|
|
70
|
+
{breadcrumbs.map((item, index) => (
|
|
71
|
+
<div key={item.label || index} className="flex items-center gap-2">
|
|
72
|
+
{index > 0 && (
|
|
73
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
74
|
+
)}
|
|
75
|
+
{item.href ? (
|
|
76
|
+
<a
|
|
77
|
+
href={item.href}
|
|
78
|
+
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
79
|
+
>
|
|
80
|
+
{item.icon && (
|
|
81
|
+
<span className="p-1 border border-border rounded">
|
|
82
|
+
{item.icon}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{item.label && <span>{item.label}</span>}
|
|
86
|
+
</a>
|
|
87
|
+
) : (
|
|
88
|
+
<span className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
89
|
+
{item.icon && (
|
|
90
|
+
<span className="p-1 border border-border rounded">
|
|
91
|
+
{item.icon}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
{item.label && <span>{item.label}</span>}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</nav>
|
|
100
|
+
|
|
101
|
+
{/* Right side actions */}
|
|
102
|
+
{sessionId && (
|
|
103
|
+
<TooltipProvider delayDuration={0}>
|
|
104
|
+
<Tooltip>
|
|
105
|
+
<TooltipTrigger asChild>
|
|
106
|
+
<Button variant="ghost" size="icon-sm" asChild>
|
|
107
|
+
<a
|
|
108
|
+
href={
|
|
109
|
+
isTimelineView
|
|
110
|
+
? `/sessions/${sessionId}`
|
|
111
|
+
: `/sessions/${sessionId}/timeline`
|
|
112
|
+
}
|
|
113
|
+
>
|
|
114
|
+
{isTimelineView ? (
|
|
115
|
+
<List className="size-4 text-muted-foreground" />
|
|
116
|
+
) : (
|
|
117
|
+
<SquareChartGantt className="size-4 text-muted-foreground" />
|
|
118
|
+
)}
|
|
119
|
+
</a>
|
|
120
|
+
</Button>
|
|
121
|
+
</TooltipTrigger>
|
|
122
|
+
<TooltipContent>
|
|
123
|
+
<p>{isTimelineView ? "Logs View" : "Timeline View"}</p>
|
|
124
|
+
</TooltipContent>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
</TooltipProvider>
|
|
127
|
+
)}
|
|
128
|
+
</header>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -735,7 +735,7 @@ export function SpanDetailsPanel({
|
|
|
735
735
|
<div className="flex flex-col gap-6 px-6 py-8">
|
|
736
736
|
{/* Header */}
|
|
737
737
|
<div className="flex items-start gap-2">
|
|
738
|
-
<SpanIcon
|
|
738
|
+
<SpanIcon span={span} className="size-6" />
|
|
739
739
|
<h2 className="flex-1 text-xl font-semibold text-foreground tracking-[-0.02em] leading-tight overflow-hidden text-ellipsis">
|
|
740
740
|
{displayName}
|
|
741
741
|
</h2>
|
|
@@ -803,7 +803,7 @@ export function SpanDetailsPanel({
|
|
|
803
803
|
</span>
|
|
804
804
|
<div className="flex items-center gap-2 flex-1 overflow-hidden">
|
|
805
805
|
<SpanIcon
|
|
806
|
-
|
|
806
|
+
span={parentSpan}
|
|
807
807
|
className="size-5 shrink-0"
|
|
808
808
|
/>
|
|
809
809
|
<span className="text-sm text-foreground truncate">
|
|
@@ -1,37 +1,88 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FoldVertical,
|
|
3
|
+
ListChecks,
|
|
4
|
+
ListTree,
|
|
5
|
+
type LucideIcon,
|
|
6
|
+
Settings,
|
|
7
|
+
Sparkles,
|
|
8
|
+
} from "lucide-react";
|
|
2
9
|
import { cn } from "@/lib/utils";
|
|
3
|
-
import
|
|
10
|
+
import { detectSpanType } from "../lib/spanTypeDetector";
|
|
11
|
+
import { getToolIcon } from "../lib/toolRegistry";
|
|
12
|
+
import type { Span } from "../types";
|
|
4
13
|
|
|
5
14
|
interface SpanIconProps {
|
|
6
|
-
|
|
15
|
+
span: Span;
|
|
7
16
|
className?: string;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
function parseAttributes(attrs: string | null): Record<string, unknown> {
|
|
20
|
+
if (!attrs) return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(attrs);
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a span is compaction-related
|
|
30
|
+
*/
|
|
31
|
+
function isCompactionSpan(span: Span): boolean {
|
|
32
|
+
const lowerName = span.name.toLowerCase();
|
|
33
|
+
return (
|
|
34
|
+
lowerName.includes("compact") ||
|
|
35
|
+
lowerName.includes("compression") ||
|
|
36
|
+
lowerName.includes("summarize") ||
|
|
37
|
+
lowerName.includes("summarization")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get icon and background color for a span
|
|
43
|
+
*/
|
|
44
|
+
function getSpanIconConfig(span: Span): {
|
|
45
|
+
icon: LucideIcon;
|
|
46
|
+
bgColor: string;
|
|
47
|
+
} {
|
|
48
|
+
const attrs = parseAttributes(span.attributes);
|
|
49
|
+
const spanType = detectSpanType(span);
|
|
50
|
+
|
|
51
|
+
// Check for compaction spans first (highest priority)
|
|
52
|
+
if (isCompactionSpan(span)) {
|
|
53
|
+
return { icon: FoldVertical, bgColor: "bg-yellow-600" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Chat spans
|
|
57
|
+
if (spanType === "chat") {
|
|
58
|
+
return { icon: Sparkles, bgColor: "bg-orange-500" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Subagent spans (Task tool)
|
|
62
|
+
if (spanType === "subagent") {
|
|
63
|
+
return { icon: ListTree, bgColor: "bg-purple-500" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Tool call spans - use specific icon based on tool name
|
|
67
|
+
if (spanType === "tool_call") {
|
|
68
|
+
const toolName = attrs["tool.name"] as string;
|
|
69
|
+
if (toolName) {
|
|
70
|
+
return { icon: getToolIcon(toolName), bgColor: "bg-blue-500" };
|
|
71
|
+
}
|
|
72
|
+
return { icon: ListChecks, bgColor: "bg-blue-500" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Default for other spans
|
|
76
|
+
return { icon: Settings, bgColor: "bg-neutral-400" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function SpanIcon({ span, className }: SpanIconProps) {
|
|
80
|
+
const { icon: Icon, bgColor } = getSpanIconConfig(span);
|
|
30
81
|
|
|
31
82
|
return (
|
|
32
83
|
<div
|
|
33
84
|
className={cn(
|
|
34
|
-
"size-5 rounded flex items-center justify-center",
|
|
85
|
+
"size-5 rounded flex items-center justify-center shrink-0",
|
|
35
86
|
bgColor,
|
|
36
87
|
className,
|
|
37
88
|
)}
|
|
@@ -134,7 +134,7 @@ function _SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
|
|
|
134
134
|
}}
|
|
135
135
|
>
|
|
136
136
|
<div className="flex items-center hover:bg-muted rounded">
|
|
137
|
-
<SpanIcon
|
|
137
|
+
<SpanIcon span={span} className="mr-2" />
|
|
138
138
|
<span className="font-medium flex-1 truncate">{displayName}</span>
|
|
139
139
|
<span className="text-muted-foreground text-sm ml-2">
|
|
140
140
|
{span.durationMs.toFixed(2)}ms
|
|
@@ -350,7 +350,6 @@ function RenderSpanTreeRow({
|
|
|
350
350
|
onSpanClick: (span: SpanNode) => void;
|
|
351
351
|
}) {
|
|
352
352
|
const attrs = parseAttributes(span.attributes);
|
|
353
|
-
const spanType = detectSpanType(span);
|
|
354
353
|
|
|
355
354
|
const isToolCall = span.name === "agent.tool_call";
|
|
356
355
|
const isChatSpan =
|
|
@@ -395,7 +394,7 @@ function RenderSpanTreeRow({
|
|
|
395
394
|
}
|
|
396
395
|
}}
|
|
397
396
|
>
|
|
398
|
-
<SpanIcon
|
|
397
|
+
<SpanIcon span={span} className="shrink-0" />
|
|
399
398
|
<span className="text-sm truncate flex-1">{displayName}</span>
|
|
400
399
|
</div>
|
|
401
400
|
{span.children.map((child) => (
|