@townco/debugger 0.1.32 → 0.1.33

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,8 @@ export function UnifiedTimeline({
187
161
  });
188
162
 
189
163
  return filtered;
190
- }, [spanTree, spans, turnLayouts]);
164
+ // biome-ignore lint/correctness/useExhaustiveDependencies: flattenSpanTree is stable
165
+ }, [spanTree, spans, turnLayouts, flattenSpanTree]);
191
166
 
192
167
  if (spans.length === 0) {
193
168
  return (
@@ -203,6 +178,7 @@ export function UnifiedTimeline({
203
178
  {/* Zoom controls */}
204
179
  <div className="flex items-center gap-4 text-sm">
205
180
  <button
181
+ type="button"
206
182
  onClick={() => setZoom((prev) => Math.max(1, prev * 0.8))}
207
183
  className="px-3 py-1 border border-border rounded hover:bg-muted"
208
184
  >
@@ -212,12 +188,14 @@ export function UnifiedTimeline({
212
188
  {(zoom * 100).toFixed(0)}%
213
189
  </span>
214
190
  <button
191
+ type="button"
215
192
  onClick={() => setZoom((prev) => Math.min(10, prev * 1.2))}
216
193
  className="px-3 py-1 border border-border rounded hover:bg-muted"
217
194
  >
218
195
  Zoom In
219
196
  </button>
220
197
  <button
198
+ type="button"
221
199
  onClick={() => setZoom(1)}
222
200
  className="px-3 py-1 border border-border rounded hover:bg-muted"
223
201
  >
@@ -290,8 +268,11 @@ export function UnifiedTimeline({
290
268
  }
291
269
 
292
270
  return (
271
+ // biome-ignore lint/a11y/useSemanticElements: complex timeline layout
293
272
  <div
294
273
  key={span.span_id}
274
+ role="button"
275
+ tabIndex={0}
295
276
  className={`h-[38px] px-3 py-2 border-b border-border flex items-center gap-2 cursor-pointer transition-colors ${
296
277
  hoveredSpanId === span.span_id ? "bg-muted" : ""
297
278
  }`}
@@ -299,6 +280,12 @@ export function UnifiedTimeline({
299
280
  onMouseEnter={() => setHoveredSpanId(span.span_id)}
300
281
  onMouseLeave={() => setHoveredSpanId(null)}
301
282
  onClick={() => setSelectedSpan(node)}
283
+ onKeyDown={(e) => {
284
+ if (e.key === "Enter" || e.key === " ") {
285
+ e.preventDefault();
286
+ setSelectedSpan(node);
287
+ }
288
+ }}
302
289
  >
303
290
  <SpanIcon type={spanType} className="w-4 h-4 shrink-0" />
304
291
  <span className="text-xs truncate">{displayName}</span>
@@ -338,11 +325,6 @@ export function UnifiedTimeline({
338
325
  left: turn.leftPercent * zoom,
339
326
  };
340
327
 
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
328
  return (
347
329
  <div key={trace.trace_id}>
348
330
  {/* User message bubble - positioned at top, aligned to start of turn */}
@@ -568,7 +550,6 @@ export function UnifiedTimeline({
568
550
  );
569
551
  }
570
552
 
571
- const attrs = parseAttributes(span.attributes);
572
553
  const spanType = detectSpanType(span);
573
554
 
574
555
  // Find which turn this span belongs to
@@ -610,8 +591,11 @@ export function UnifiedTimeline({
610
591
  1_000_000;
611
592
 
612
593
  return (
594
+ // biome-ignore lint/a11y/useSemanticElements: positioned timeline bar
613
595
  <div
614
596
  key={span.span_id}
597
+ role="button"
598
+ tabIndex={0}
615
599
  className={`absolute h-6 ${barColor} rounded cursor-pointer transition-all flex items-center justify-start px-2 ${
616
600
  hoveredSpanId === span.span_id ? "brightness-110" : ""
617
601
  }`}
@@ -625,6 +609,12 @@ export function UnifiedTimeline({
625
609
  onClick={() => {
626
610
  setSelectedSpan(node);
627
611
  }}
612
+ onKeyDown={(e) => {
613
+ if (e.key === "Enter" || e.key === " ") {
614
+ e.preventDefault();
615
+ setSelectedSpan(node);
616
+ }
617
+ }}
628
618
  >
629
619
  <span className="text-[11px] text-white font-medium whitespace-nowrap">
630
620
  {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
  }