@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.
@@ -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 type={spanType} className="size-6" />
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
- type={detectSpanType(parentSpan)}
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 { ListChecks, Search, Settings, Sparkles } from "lucide-react";
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 type { SpanType } from "../lib/spanTypeDetector";
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
- type: SpanType;
15
+ span: Span;
7
16
  className?: string;
8
17
  }
9
18
 
10
- export function SpanIcon({ type, className }: SpanIconProps) {
11
- // Background color for the icon box
12
- const bgColor =
13
- type === "chat"
14
- ? "bg-orange-400"
15
- : type === "tool_call"
16
- ? "bg-blue-500"
17
- : type === "subagent"
18
- ? "bg-purple-500"
19
- : "bg-blue-500";
20
-
21
- // Choose icon based on type
22
- const Icon =
23
- type === "chat"
24
- ? Sparkles
25
- : type === "tool_call"
26
- ? ListChecks
27
- : type === "subagent"
28
- ? Search
29
- : Settings;
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 type={spanType} className="mr-2" />
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 type={spanType} className="shrink-0" />
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) => (