@townco/debugger 0.1.32 → 0.1.34

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.
@@ -211,7 +211,7 @@ export function SpanDetailsPanel({
211
211
  const CollapsibleText = ({ text }: { text: string }) => {
212
212
  const [expanded, setExpanded] = useState(false);
213
213
  const shouldCollapse = text.length > 300;
214
- const preview = shouldCollapse ? text.slice(0, 300) + "..." : text;
214
+ const preview = shouldCollapse ? `${text.slice(0, 300)}...` : text;
215
215
 
216
216
  if (!shouldCollapse) {
217
217
  return (
@@ -247,6 +247,7 @@ export function SpanDetailsPanel({
247
247
  }: {
248
248
  title: string;
249
249
  subtitle?: string;
250
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
250
251
  content: any;
251
252
  borderColor: string;
252
253
  bgColor: string;
@@ -303,11 +304,14 @@ export function SpanDetailsPanel({
303
304
  result,
304
305
  }: {
305
306
  toolName: string;
307
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
306
308
  input: any;
309
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
307
310
  result?: any;
308
311
  }) => {
309
312
  const [expanded, setExpanded] = useState(false);
310
313
 
314
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
311
315
  const parseContent = (content: any) => {
312
316
  if (typeof content === "string") {
313
317
  try {
@@ -319,6 +323,7 @@ export function SpanDetailsPanel({
319
323
  return content;
320
324
  };
321
325
 
326
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
322
327
  const formatContent = (content: any) => {
323
328
  const parsed = parseContent(content);
324
329
  return typeof parsed === "string"
@@ -386,6 +391,7 @@ export function SpanDetailsPanel({
386
391
 
387
392
  if (!Array.isArray(messages) || messages.length === 0) return null;
388
393
 
394
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
389
395
  const parseToolInput = (input: any): any => {
390
396
  if (typeof input === "string") {
391
397
  try {
@@ -397,6 +403,7 @@ export function SpanDetailsPanel({
397
403
  return input;
398
404
  };
399
405
 
406
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
400
407
  const renderContent = (content: any) => {
401
408
  // Handle string content - parse if it looks like JSON array
402
409
  if (typeof content === "string") {
@@ -418,15 +425,18 @@ export function SpanDetailsPanel({
418
425
  // Handle array content (can contain text and tool_use blocks)
419
426
  if (Array.isArray(content)) {
420
427
  // First, group tool_use with their corresponding tool_result
428
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
421
429
  const processedBlocks: any[] = [];
422
430
  const usedResultIndices = new Set<number>();
423
431
 
432
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
424
433
  content.forEach((block: any, blockIndex: number) => {
425
434
  if (block.type === "text") {
426
435
  processedBlocks.push({ type: "text", block, index: blockIndex });
427
436
  } else if (block.type === "tool_use") {
428
437
  // Find the corresponding tool_result
429
438
  const resultIndex = content.findIndex(
439
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
430
440
  (b: any, idx: number) =>
431
441
  b.type === "tool_result" &&
432
442
  b.tool_use_id === block.id &&
@@ -464,6 +474,7 @@ export function SpanDetailsPanel({
464
474
 
465
475
  return (
466
476
  <div className="flex flex-col gap-2">
477
+ {/* biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content */}
467
478
  {processedBlocks.map((item: any) => {
468
479
  if (item.type === "text") {
469
480
  return (
@@ -544,9 +555,11 @@ export function SpanDetailsPanel({
544
555
  <div className="p-3 flex flex-col gap-3">
545
556
  {(() => {
546
557
  // Group tool_use (from AI messages) with tool results (from tool messages)
558
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
547
559
  const processedMessages: any[] = [];
548
560
  const usedToolMessageIndices = new Set<number>();
549
561
 
562
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
550
563
  messages.forEach((message: any, msgIndex: number) => {
551
564
  const role = message.role || "unknown";
552
565
 
@@ -566,7 +579,9 @@ export function SpanDetailsPanel({
566
579
 
567
580
  if (Array.isArray(parsedContent)) {
568
581
  // Extract tool_use blocks
582
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
569
583
  const toolUses: any[] = [];
584
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic OTEL content
570
585
  const nonToolBlocks: any[] = [];
571
586
 
572
587
  for (const block of parsedContent) {
@@ -726,6 +741,7 @@ export function SpanDetailsPanel({
726
741
  </h2>
727
742
  <Dialog.Close asChild>
728
743
  <button
744
+ type="button"
729
745
  className="size-6 flex items-center justify-center rounded hover:bg-muted transition-colors"
730
746
  aria-label="Close"
731
747
  >
@@ -737,6 +753,7 @@ export function SpanDetailsPanel({
737
753
  {/* Tabs */}
738
754
  <div className="flex items-center gap-2">
739
755
  <button
756
+ type="button"
740
757
  onClick={() => setActiveTab("run")}
741
758
  className={cn(
742
759
  "px-3 py-1 text-sm font-medium rounded-lg transition-colors",
@@ -748,6 +765,7 @@ export function SpanDetailsPanel({
748
765
  Run
749
766
  </button>
750
767
  <button
768
+ type="button"
751
769
  onClick={() => setActiveTab("logs")}
752
770
  className={cn(
753
771
  "px-3 py-1 text-sm font-medium rounded-lg transition-colors",
@@ -759,6 +777,7 @@ export function SpanDetailsPanel({
759
777
  Logs
760
778
  </button>
761
779
  <button
780
+ type="button"
762
781
  onClick={() => setActiveTab("feedback")}
763
782
  className={cn(
764
783
  "px-3 py-1 text-sm font-medium rounded-lg transition-colors",
@@ -55,7 +55,7 @@ interface SpanRowProps {
55
55
  traceEnd: number;
56
56
  }
57
57
 
58
- function SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
58
+ function _SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
59
59
  const [expanded, setExpanded] = useState(false);
60
60
  const [showRaw, setShowRaw] = useState(false);
61
61
 
@@ -119,10 +119,19 @@ function SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
119
119
 
120
120
  return (
121
121
  <div>
122
+ {/* biome-ignore lint/a11y/useSemanticElements: complex layout with nested content */}
122
123
  <div
124
+ role="button"
125
+ tabIndex={hasDetails ? 0 : undefined}
123
126
  className={`py-1 px-2 ${hasDetails ? "cursor-pointer" : ""}`}
124
127
  style={{ paddingLeft: span.depth * 20 + 8 }}
125
128
  onClick={() => hasDetails && setExpanded(!expanded)}
129
+ onKeyDown={(e) => {
130
+ if (hasDetails && (e.key === "Enter" || e.key === " ")) {
131
+ e.preventDefault();
132
+ setExpanded(!expanded);
133
+ }
134
+ }}
126
135
  >
127
136
  <div className="flex items-center hover:bg-muted rounded">
128
137
  <SpanIcon type={spanType} className="mr-2" />
@@ -236,7 +245,7 @@ function SpanRow({ span, traceStart, traceEnd }: SpanRowProps) {
236
245
  )}
237
246
 
238
247
  {span.children.map((child) => (
239
- <SpanRow
248
+ <_SpanRow
240
249
  key={child.span_id}
241
250
  span={child}
242
251
  traceStart={traceStart}
@@ -301,8 +310,8 @@ export function SpanTimeline({
301
310
  <div className="flex-1 overflow-x-auto">
302
311
  {/* Timeline header with time markers */}
303
312
  <div className="h-6 px-4 py-1 bg-muted border-b border-border flex items-center text-[10px] text-muted-foreground gap-16">
304
- {timeMarkers.map((marker, i) => (
305
- <span key={i} className="w-[260px]">
313
+ {timeMarkers.map((marker) => (
314
+ <span key={marker} className="w-[260px]">
306
315
  {marker}
307
316
  </span>
308
317
  ))}
@@ -372,10 +381,19 @@ function RenderSpanTreeRow({
372
381
 
373
382
  return (
374
383
  <>
384
+ {/* biome-ignore lint/a11y/useSemanticElements: complex layout row */}
375
385
  <div
386
+ role="button"
387
+ tabIndex={0}
376
388
  className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer transition-colors"
377
389
  style={{ paddingLeft: `${span.depth * 20 + 16}px` }}
378
390
  onClick={() => onSpanClick(span)}
391
+ onKeyDown={(e) => {
392
+ if (e.key === "Enter" || e.key === " ") {
393
+ e.preventDefault();
394
+ onSpanClick(span);
395
+ }
396
+ }}
379
397
  >
380
398
  <SpanIcon type={spanType} className="shrink-0" />
381
399
  <span className="text-sm truncate flex-1">{displayName}</span>
@@ -422,9 +440,18 @@ function RenderSpanTreeTimeline({
422
440
 
423
441
  return (
424
442
  <>
443
+ {/* biome-ignore lint/a11y/useSemanticElements: complex timeline bar layout */}
425
444
  <div
445
+ role="button"
446
+ tabIndex={0}
426
447
  className="h-[38px] border-b border-border relative flex items-center px-4 hover:bg-muted/50 cursor-pointer transition-colors"
427
448
  onClick={() => onSpanClick(span)}
449
+ onKeyDown={(e) => {
450
+ if (e.key === "Enter" || e.key === " ") {
451
+ e.preventDefault();
452
+ onSpanClick(span);
453
+ }
454
+ }}
428
455
  >
429
456
  <div
430
457
  className={`absolute h-[18px] rounded ${barColor} flex items-center px-1 pointer-events-none`}
@@ -53,7 +53,7 @@ function SpanRow({
53
53
  span: SpanNode;
54
54
  onSpanClick: (span: SpanNode) => void;
55
55
  }) {
56
- const [expanded, setExpanded] = useState(false);
56
+ const [expanded, _setExpanded] = useState(false);
57
57
  const [showRaw, setShowRaw] = useState(false);
58
58
 
59
59
  const attrs = parseAttributes(span.attributes);
@@ -104,10 +104,19 @@ function SpanRow({
104
104
 
105
105
  return (
106
106
  <div>
107
+ {/* biome-ignore lint/a11y/useSemanticElements: complex tree layout */}
107
108
  <div
109
+ role="button"
110
+ tabIndex={hasDetails ? 0 : undefined}
108
111
  className={`flex items-center py-1 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
109
112
  style={{ paddingLeft: span.depth * 20 + 8 }}
110
113
  onClick={() => onSpanClick(span)}
114
+ onKeyDown={(e) => {
115
+ if (e.key === "Enter" || e.key === " ") {
116
+ e.preventDefault();
117
+ onSpanClick(span);
118
+ }
119
+ }}
111
120
  >
112
121
  <span className={`${statusColor} mr-2`}>●</span>
113
122
  <span className="font-medium flex-1 truncate">{displayName}</span>
@@ -1,4 +1,3 @@
1
- import { Card, CardContent } from "@/components/ui/card";
2
1
  import type { TokenUsage } from "../lib/turnExtractor";
3
2
 
4
3
  interface TurnMetadataPanelProps {
@@ -22,20 +22,6 @@ function parseAttributes(attrs: string | null): Record<string, unknown> {
22
22
  }
23
23
  }
24
24
 
25
- // Generate distinct colors for turns
26
- const TURN_COLORS = [
27
- "bg-blue-500/10",
28
- "bg-purple-500/10",
29
- "bg-green-500/10",
30
- "bg-orange-500/10",
31
- "bg-pink-500/10",
32
- "bg-yellow-500/10",
33
- "bg-red-500/10",
34
- "bg-indigo-500/10",
35
- "bg-teal-500/10",
36
- "bg-cyan-500/10",
37
- ];
38
-
39
25
  export function UnifiedTimeline({
40
26
  spans,
41
27
  traces,
@@ -91,16 +77,6 @@ export function UnifiedTimeline({
91
77
  });
92
78
  }, [traces, traceData]);
93
79
 
94
- // Helper to find span node in tree
95
- const findSpanNode = (nodes: SpanNode[], spanId: string): SpanNode | null => {
96
- for (const node of nodes) {
97
- if (node.span_id === spanId) return node;
98
- const found = findSpanNode(node.children, spanId);
99
- if (found) return found;
100
- }
101
- return null;
102
- };
103
-
104
80
  // Calculate cumulative width for each turn
105
81
  const turnLayouts = useMemo(() => {
106
82
  let cumulativePercent = 0;
@@ -130,18 +106,16 @@ export function UnifiedTimeline({
130
106
  }, [turnBoundaries]);
131
107
 
132
108
  // Calculate global timeline bounds
133
- const globalStart = useMemo(() => {
109
+ const _globalStart = useMemo(() => {
134
110
  if (spans.length === 0) return 0;
135
111
  return Math.min(...spans.map((s) => s.start_time_unix_nano));
136
112
  }, [spans]);
137
113
 
138
- const globalEnd = useMemo(() => {
114
+ const _globalEnd = useMemo(() => {
139
115
  if (spans.length === 0) return 0;
140
116
  return Math.max(...spans.map((s) => s.end_time_unix_nano));
141
117
  }, [spans]);
142
118
 
143
- const totalDurationMs = (globalEnd - globalStart) / 1_000_000;
144
-
145
119
  // Zoom handler (scroll is now native)
146
120
  const handleWheel = useCallback((e: React.WheelEvent) => {
147
121
  if (e.ctrlKey || e.metaKey) {
@@ -187,7 +161,7 @@ export function UnifiedTimeline({
187
161
  });
188
162
 
189
163
  return filtered;
190
- }, [spanTree, spans, turnLayouts]);
164
+ }, [spanTree, spans, turnLayouts, flattenSpanTree]);
191
165
 
192
166
  if (spans.length === 0) {
193
167
  return (
@@ -203,6 +177,7 @@ export function UnifiedTimeline({
203
177
  {/* Zoom controls */}
204
178
  <div className="flex items-center gap-4 text-sm">
205
179
  <button
180
+ type="button"
206
181
  onClick={() => setZoom((prev) => Math.max(1, prev * 0.8))}
207
182
  className="px-3 py-1 border border-border rounded hover:bg-muted"
208
183
  >
@@ -212,12 +187,14 @@ export function UnifiedTimeline({
212
187
  {(zoom * 100).toFixed(0)}%
213
188
  </span>
214
189
  <button
190
+ type="button"
215
191
  onClick={() => setZoom((prev) => Math.min(10, prev * 1.2))}
216
192
  className="px-3 py-1 border border-border rounded hover:bg-muted"
217
193
  >
218
194
  Zoom In
219
195
  </button>
220
196
  <button
197
+ type="button"
221
198
  onClick={() => setZoom(1)}
222
199
  className="px-3 py-1 border border-border rounded hover:bg-muted"
223
200
  >
@@ -290,8 +267,11 @@ export function UnifiedTimeline({
290
267
  }
291
268
 
292
269
  return (
270
+ // biome-ignore lint/a11y/useSemanticElements: complex timeline layout
293
271
  <div
294
272
  key={span.span_id}
273
+ role="button"
274
+ tabIndex={0}
295
275
  className={`h-[38px] px-3 py-2 border-b border-border flex items-center gap-2 cursor-pointer transition-colors ${
296
276
  hoveredSpanId === span.span_id ? "bg-muted" : ""
297
277
  }`}
@@ -299,6 +279,12 @@ export function UnifiedTimeline({
299
279
  onMouseEnter={() => setHoveredSpanId(span.span_id)}
300
280
  onMouseLeave={() => setHoveredSpanId(null)}
301
281
  onClick={() => setSelectedSpan(node)}
282
+ onKeyDown={(e) => {
283
+ if (e.key === "Enter" || e.key === " ") {
284
+ e.preventDefault();
285
+ setSelectedSpan(node);
286
+ }
287
+ }}
302
288
  >
303
289
  <SpanIcon type={spanType} className="w-4 h-4 shrink-0" />
304
290
  <span className="text-xs truncate">{displayName}</span>
@@ -338,11 +324,6 @@ export function UnifiedTimeline({
338
324
  left: turn.leftPercent * zoom,
339
325
  };
340
326
 
341
- // Agent message positioned at end of turn (100% of turn width)
342
- const agentPos = {
343
- left: (turn.leftPercent + turn.widthPercent) * zoom,
344
- };
345
-
346
327
  return (
347
328
  <div key={trace.trace_id}>
348
329
  {/* User message bubble - positioned at top, aligned to start of turn */}
@@ -568,7 +549,6 @@ export function UnifiedTimeline({
568
549
  );
569
550
  }
570
551
 
571
- const attrs = parseAttributes(span.attributes);
572
552
  const spanType = detectSpanType(span);
573
553
 
574
554
  // Find which turn this span belongs to
@@ -610,8 +590,11 @@ export function UnifiedTimeline({
610
590
  1_000_000;
611
591
 
612
592
  return (
593
+ // biome-ignore lint/a11y/useSemanticElements: positioned timeline bar
613
594
  <div
614
595
  key={span.span_id}
596
+ role="button"
597
+ tabIndex={0}
615
598
  className={`absolute h-6 ${barColor} rounded cursor-pointer transition-all flex items-center justify-start px-2 ${
616
599
  hoveredSpanId === span.span_id ? "brightness-110" : ""
617
600
  }`}
@@ -625,6 +608,12 @@ export function UnifiedTimeline({
625
608
  onClick={() => {
626
609
  setSelectedSpan(node);
627
610
  }}
611
+ onKeyDown={(e) => {
612
+ if (e.key === "Enter" || e.key === " ") {
613
+ e.preventDefault();
614
+ setSelectedSpan(node);
615
+ }
616
+ }}
628
617
  >
629
618
  <span className="text-[11px] text-white font-medium whitespace-nowrap">
630
619
  {durationMs.toFixed(2)}ms
@@ -1,6 +1,6 @@
1
1
  import { Slot } from "@radix-ui/react-slot";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
- import * as React from "react";
3
+ import type * as React from "react";
4
4
 
5
5
  import { cn } from "@/lib/utils";
6
6
 
@@ -1,4 +1,4 @@
1
- import * as React from "react";
1
+ import type * as React from "react";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
4
 
@@ -14,6 +14,7 @@ interface CheckboxProps {
14
14
  const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
15
15
  ({ id, checked = false, onCheckedChange, disabled, className }, ref) => {
16
16
  return (
17
+ // biome-ignore lint/a11y/useSemanticElements: custom checkbox component using button for styling control
17
18
  <button
18
19
  ref={ref}
19
20
  type="button"
@@ -1,4 +1,4 @@
1
- import * as React from "react";
1
+ import type * as React from "react";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
4
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as LabelPrimitive from "@radix-ui/react-label";
4
- import * as React from "react";
4
+ import type * as React from "react";
5
5
 
6
6
  import { cn } from "@/lib/utils";
7
7
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as SelectPrimitive from "@radix-ui/react-select";
4
4
  import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5
- import * as React from "react";
5
+ import type * as React from "react";
6
6
 
7
7
  import { cn } from "@/lib/utils";
8
8
 
@@ -1,4 +1,4 @@
1
- import * as React from "react";
1
+ import type * as React from "react";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
4
 
package/src/frontend.tsx CHANGED
@@ -9,6 +9,7 @@ import { StrictMode } from "react";
9
9
  import { createRoot } from "react-dom/client";
10
10
  import { App } from "./App";
11
11
 
12
+ // biome-ignore lint/style/noNonNullAssertion: root element always exists
12
13
  const elem = document.getElementById("root")!;
13
14
  const app = (
14
15
  <StrictMode>
@@ -18,6 +19,7 @@ const app = (
18
19
 
19
20
  if (import.meta.hot) {
20
21
  // With hot module reloading, `import.meta.hot.data` is persisted.
22
+ // biome-ignore lint/suspicious/noAssignInExpressions: HMR pattern
21
23
  const root = (import.meta.hot.data.root ??= createRoot(elem));
22
24
  root.render(app);
23
25
  } else {
@@ -29,10 +29,12 @@ describe("metrics", () => {
29
29
  "gen_ai.usage.input_tokens": 100,
30
30
  "gen_ai.usage.output_tokens": 50,
31
31
  }),
32
+ // biome-ignore lint/suspicious/noExplicitAny: test mock
32
33
  } as any,
33
34
  {
34
35
  name: "tool_call:search",
35
36
  attributes: "{}",
37
+ // biome-ignore lint/suspicious/noExplicitAny: test mock
36
38
  } as any,
37
39
  ];
38
40
 
@@ -117,12 +117,40 @@ export function extractTurnMessages(spans: Span[], logs?: Log[]): TurnMessages {
117
117
  } else if (event.type === "tool_call") {
118
118
  const toolName = attrs["tool.name"] as string;
119
119
  if (toolName) {
120
+ // Parse tool input and output
121
+ let toolInput: unknown = null;
122
+ let toolOutput: unknown = null;
123
+
124
+ const rawInput = attrs["tool.input"];
125
+ if (rawInput) {
126
+ try {
127
+ toolInput =
128
+ typeof rawInput === "string" ? JSON.parse(rawInput) : rawInput;
129
+ } catch {
130
+ toolInput = rawInput;
131
+ }
132
+ }
133
+
134
+ const rawOutput = attrs["tool.output"];
135
+ if (rawOutput) {
136
+ try {
137
+ toolOutput =
138
+ typeof rawOutput === "string"
139
+ ? JSON.parse(rawOutput)
140
+ : rawOutput;
141
+ } catch {
142
+ toolOutput = rawOutput;
143
+ }
144
+ }
145
+
120
146
  result.agentMessages.push({
121
147
  content: toolName,
122
148
  spanId: event.span.span_id,
123
149
  timestamp: event.span.end_time_unix_nano,
124
150
  type: "tool_call",
125
151
  toolName,
152
+ toolInput,
153
+ toolOutput,
126
154
  });
127
155
  }
128
156
  }