@oh-my-pi/omp-stats 18.3.5 → 18.4.1

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.
Files changed (192) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +13 -11
  3. package/build.ts +5 -49
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/index.js +105 -126
  7. package/dist/types/aggregator.d.ts +25 -22
  8. package/dist/types/client/api.d.ts +11 -7
  9. package/dist/types/client/app/LiveChip.d.ts +2 -0
  10. package/dist/types/client/app/Shell.d.ts +16 -0
  11. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  12. package/dist/types/client/app/nav.d.ts +18 -0
  13. package/dist/types/client/charts/BarList.d.ts +17 -0
  14. package/dist/types/client/charts/Chart.d.ts +49 -0
  15. package/dist/types/client/charts/Legend.d.ts +16 -0
  16. package/dist/types/client/charts/ShareBar.d.ts +11 -0
  17. package/dist/types/client/charts/Sparkline.d.ts +11 -0
  18. package/dist/types/client/charts/TimeChart.d.ts +9 -0
  19. package/dist/types/client/charts/index.d.ts +7 -0
  20. package/dist/types/client/charts/types.d.ts +25 -0
  21. package/dist/types/client/charts/useWidth.d.ts +3 -0
  22. package/dist/types/client/data/colors.d.ts +25 -0
  23. package/dist/types/client/data/formatters.d.ts +9 -1
  24. package/dist/types/client/data/live.d.ts +29 -0
  25. package/dist/types/client/data/query.d.ts +35 -0
  26. package/dist/types/client/data/range.d.ts +31 -0
  27. package/dist/types/client/data/series.d.ts +28 -0
  28. package/dist/types/client/data/useHashRoute.d.ts +18 -4
  29. package/dist/types/client/data/view-models.d.ts +111 -28
  30. package/dist/types/client/index.d.ts +0 -1
  31. package/dist/types/client/routes/CostsRoute.d.ts +2 -2
  32. package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
  33. package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
  34. package/dist/types/client/routes/GainRoute.d.ts +1 -2
  35. package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
  36. package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
  37. package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
  38. package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
  39. package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
  40. package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
  41. package/dist/types/client/routes/TracesRoute.d.ts +2 -2
  42. package/dist/types/client/routes/index.d.ts +1 -1
  43. package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
  44. package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
  45. package/dist/types/client/traces/trace-colors.d.ts +18 -12
  46. package/dist/types/client/ui/Badge.d.ts +19 -0
  47. package/dist/types/client/ui/Card.d.ts +25 -0
  48. package/dist/types/client/ui/Drawer.d.ts +25 -0
  49. package/dist/types/client/ui/JsonBlock.d.ts +2 -2
  50. package/dist/types/client/ui/Modal.d.ts +23 -0
  51. package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
  52. package/dist/types/client/ui/SearchInput.d.ts +8 -0
  53. package/dist/types/client/ui/Segmented.d.ts +15 -0
  54. package/dist/types/client/ui/Stat.d.ts +36 -0
  55. package/dist/types/client/ui/States.d.ts +41 -0
  56. package/dist/types/client/ui/Table.d.ts +48 -0
  57. package/dist/types/client/ui/index.d.ts +9 -10
  58. package/dist/types/db.d.ts +90 -106
  59. package/dist/types/frustration.d.ts +70 -0
  60. package/dist/types/index.d.ts +3 -1
  61. package/dist/types/live.d.ts +30 -0
  62. package/dist/types/rollup.d.ts +115 -0
  63. package/dist/types/server.d.ts +6 -1
  64. package/dist/types/shared-types.d.ts +116 -56
  65. package/dist/types/types.d.ts +4 -0
  66. package/dist/types/user-metrics.d.ts +8 -0
  67. package/package.json +7 -13
  68. package/src/aggregator.ts +276 -211
  69. package/src/client/App.tsx +38 -80
  70. package/src/client/api.ts +65 -7
  71. package/src/client/app/LiveChip.tsx +97 -0
  72. package/src/client/app/Shell.tsx +181 -0
  73. package/src/client/app/ThemeToggle.tsx +5 -2
  74. package/src/client/app/nav.ts +78 -0
  75. package/src/client/charts/BarList.tsx +48 -0
  76. package/src/client/charts/Chart.tsx +388 -0
  77. package/src/client/charts/Legend.tsx +61 -0
  78. package/src/client/charts/ShareBar.tsx +26 -0
  79. package/src/client/charts/Sparkline.tsx +38 -0
  80. package/src/client/charts/TimeChart.tsx +22 -0
  81. package/src/client/charts/index.ts +7 -0
  82. package/src/client/charts/types.ts +27 -0
  83. package/src/client/charts/useWidth.ts +19 -0
  84. package/src/client/data/colors.ts +45 -0
  85. package/src/client/data/formatters.ts +30 -9
  86. package/src/client/data/live.tsx +85 -0
  87. package/src/client/data/query.ts +166 -0
  88. package/src/client/data/range.ts +82 -0
  89. package/src/client/data/series.ts +85 -0
  90. package/src/client/data/useHashRoute.ts +41 -83
  91. package/src/client/data/view-models.ts +347 -140
  92. package/src/client/index.tsx +3 -6
  93. package/src/client/routes/CostsRoute.tsx +394 -222
  94. package/src/client/routes/ErrorsRoute.tsx +401 -98
  95. package/src/client/routes/FrustrationRoute.tsx +924 -0
  96. package/src/client/routes/GainRoute.tsx +209 -206
  97. package/src/client/routes/ModelsRoute.tsx +540 -431
  98. package/src/client/routes/OverviewRoute.tsx +283 -311
  99. package/src/client/routes/ProjectsRoute.tsx +298 -156
  100. package/src/client/routes/ProvidersRoute.tsx +948 -416
  101. package/src/client/routes/RequestsRoute.tsx +295 -107
  102. package/src/client/routes/ToolsRoute.tsx +495 -398
  103. package/src/client/routes/TracesRoute.tsx +136 -117
  104. package/src/client/routes/costs.css +24 -0
  105. package/src/client/routes/errors.css +60 -0
  106. package/src/client/routes/frustration.css +123 -0
  107. package/src/client/routes/index.ts +1 -1
  108. package/src/client/routes/models.css +43 -0
  109. package/src/client/routes/projects.css +6 -0
  110. package/src/client/routes/providers.css +28 -0
  111. package/src/client/routes/tools.css +14 -0
  112. package/src/client/styles.css +1396 -1299
  113. package/src/client/traces/AggregatesPanel.tsx +55 -74
  114. package/src/client/traces/Minimap.tsx +6 -15
  115. package/src/client/traces/SpanDrawer.tsx +128 -194
  116. package/src/client/traces/SummaryStrip.tsx +21 -24
  117. package/src/client/traces/TimelineCanvas.tsx +27 -44
  118. package/src/client/traces/TraceView.tsx +175 -130
  119. package/src/client/traces/TranscriptList.tsx +13 -41
  120. package/src/client/traces/trace-colors.ts +60 -53
  121. package/src/client/traces/traces.css +279 -0
  122. package/src/client/ui/Badge.tsx +29 -0
  123. package/src/client/ui/Card.tsx +61 -0
  124. package/src/client/ui/Drawer.tsx +93 -0
  125. package/src/client/ui/JsonBlock.tsx +26 -44
  126. package/src/client/ui/Modal.tsx +106 -0
  127. package/src/client/ui/RequestDrawer.tsx +163 -188
  128. package/src/client/ui/SearchInput.tsx +25 -0
  129. package/src/client/ui/Segmented.tsx +61 -0
  130. package/src/client/ui/Stat.tsx +104 -0
  131. package/src/client/ui/States.tsx +81 -0
  132. package/src/client/ui/Table.tsx +229 -0
  133. package/src/client/ui/index.ts +9 -10
  134. package/src/client/ui/modal.css +54 -0
  135. package/src/client/ui/request-drawer.css +40 -0
  136. package/src/db.ts +379 -983
  137. package/src/frustration.ts +473 -0
  138. package/src/index.ts +22 -22
  139. package/src/live.ts +259 -0
  140. package/src/parser.ts +55 -66
  141. package/src/rollup.ts +951 -0
  142. package/src/server.ts +143 -87
  143. package/src/shared-types.ts +120 -56
  144. package/src/trace.ts +14 -7
  145. package/src/types.ts +4 -0
  146. package/src/user-metrics.ts +13 -0
  147. package/dist/client/styles.css +0 -1976
  148. package/dist/types/client/app/AppLayout.d.ts +0 -16
  149. package/dist/types/client/app/NavRail.d.ts +0 -7
  150. package/dist/types/client/app/RangeControl.d.ts +0 -7
  151. package/dist/types/client/app/SyncButton.d.ts +0 -14
  152. package/dist/types/client/app/TopBar.d.ts +0 -15
  153. package/dist/types/client/app/routes.d.ts +0 -12
  154. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  155. package/dist/types/client/components/chart-shared.d.ts +0 -178
  156. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  157. package/dist/types/client/components/range-meta.d.ts +0 -21
  158. package/dist/types/client/data/charts.d.ts +0 -1
  159. package/dist/types/client/data/useResource.d.ts +0 -13
  160. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  161. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  162. package/dist/types/client/ui/DataTable.d.ts +0 -17
  163. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  164. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  165. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  166. package/dist/types/client/ui/Panel.d.ts +0 -7
  167. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  168. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  169. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  170. package/src/client/app/AppLayout.tsx +0 -93
  171. package/src/client/app/NavRail.tsx +0 -44
  172. package/src/client/app/RangeControl.tsx +0 -39
  173. package/src/client/app/SyncButton.tsx +0 -75
  174. package/src/client/app/TopBar.tsx +0 -73
  175. package/src/client/app/routes.ts +0 -93
  176. package/src/client/components/AgentTokenShare.tsx +0 -68
  177. package/src/client/components/chart-shared.tsx +0 -273
  178. package/src/client/components/models-table-shared.tsx +0 -255
  179. package/src/client/components/range-meta.ts +0 -73
  180. package/src/client/data/charts.ts +0 -14
  181. package/src/client/data/useResource.ts +0 -154
  182. package/src/client/routes/BehaviorRoute.tsx +0 -623
  183. package/src/client/ui/AsyncBoundary.tsx +0 -54
  184. package/src/client/ui/DataTable.tsx +0 -122
  185. package/src/client/ui/EmptyState.tsx +0 -16
  186. package/src/client/ui/ErrorState.tsx +0 -25
  187. package/src/client/ui/MetricCluster.tsx +0 -92
  188. package/src/client/ui/Panel.tsx +0 -24
  189. package/src/client/ui/SegmentedControl.tsx +0 -36
  190. package/src/client/ui/Skeleton.tsx +0 -17
  191. package/src/client/ui/StatusPill.tsx +0 -15
  192. package/tailwind.config.js +0 -40
@@ -7,11 +7,11 @@
7
7
 
8
8
  import { ChevronDown, ChevronRight } from "lucide-react";
9
9
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+ import { createPortal } from "react-dom";
10
11
  import { formatDurationMs } from "../data/formatters";
11
12
  import type { TraceMarker, TraceSpan, TraceSpanKind, TraceTrack } from "../types";
12
- import { useSystemTheme } from "../useSystemTheme";
13
13
  import { buildTicks, formatOffset, type TraceScale } from "./time-scale";
14
- import { TRACE_THEMES, type TraceTheme } from "./trace-colors";
14
+ import { type TraceTheme, useTraceTheme } from "./trace-colors";
15
15
 
16
16
  export interface TimelineViewport {
17
17
  u0: number;
@@ -166,8 +166,7 @@ export function TimelineCanvas({
166
166
  onToggleCollapse,
167
167
  traceStart,
168
168
  }: TimelineCanvasProps) {
169
- const theme = useSystemTheme();
170
- const colors = TRACE_THEMES[theme];
169
+ const colors = useTraceTheme();
171
170
  const containerRef = useRef<HTMLDivElement>(null);
172
171
  const canvasRef = useRef<HTMLCanvasElement>(null);
173
172
  const [canvasWidth, setCanvasWidth] = useState(800);
@@ -416,7 +415,7 @@ export function TimelineCanvas({
416
415
  const tooltip = hover ? renderTooltip(hover, traceStart) : null;
417
416
 
418
417
  return (
419
- <div ref={containerRef} className="stats-trace-timeline">
418
+ <div ref={containerRef} className="traces-timeline">
420
419
  {/* Gutter: track/lane labels + collapse chevrons (DOM, not canvas). */}
421
420
  <div style={{ width: GUTTER_W, flexShrink: 0, position: "relative", height: layout.totalHeight }}>
422
421
  {layout.blocks.map(block => (
@@ -443,18 +442,18 @@ export function TimelineCanvas({
443
442
  ? `Expand ${block.track.label}`
444
443
  : `Collapse ${block.track.label}`
445
444
  }
446
- className="stats-trace-chevron"
445
+ className="traces-chevron"
447
446
  >
448
447
  {collapsed.has(block.track.id) ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
449
448
  </button>
450
449
  ) : (
451
450
  <span style={{ width: 12, flexShrink: 0 }} />
452
451
  )}
453
- <span className="stats-trace-gutter-track truncate" style={{ minWidth: 0 }}>
452
+ <span className="traces-gutter-track truncate" style={{ minWidth: 0 }}>
454
453
  {block.track.label}
455
454
  </span>
456
455
  {block.track.model && (
457
- <span className="stats-trace-gutter-model truncate" style={{ flexShrink: 1, minWidth: 0 }}>
456
+ <span className="traces-gutter-model mono truncate" style={{ flexShrink: 1, minWidth: 0 }}>
458
457
  {block.track.model}
459
458
  </span>
460
459
  )}
@@ -462,7 +461,7 @@ export function TimelineCanvas({
462
461
  {block.lanes.map(lane => (
463
462
  <div
464
463
  key={`${lane.track.id}:${lane.kind}`}
465
- className="stats-trace-gutter-lane"
464
+ className="traces-gutter-lane"
466
465
  style={{ top: lane.y, left: 20 + block.depth * 14, lineHeight: `${LANE_H}px` }}
467
466
  >
468
467
  {lane.label}
@@ -474,7 +473,7 @@ export function TimelineCanvas({
474
473
 
475
474
  <canvas
476
475
  ref={canvasRef}
477
- className="stats-trace-canvas"
476
+ className="traces-canvas"
478
477
  width={Math.floor(canvasWidth * devicePixelRatio)}
479
478
  height={Math.floor(layout.totalHeight * devicePixelRatio)}
480
479
  style={{ width: canvasWidth, height: layout.totalHeight }}
@@ -492,7 +491,7 @@ export function TimelineCanvas({
492
491
  onKeyDown={handleKeyDown}
493
492
  />
494
493
 
495
- {tooltip}
494
+ {tooltip && createPortal(tooltip, document.body)}
496
495
  </div>
497
496
  );
498
497
  }
@@ -573,14 +572,14 @@ function draw(
573
572
  ctx.stroke();
574
573
  ctx.restore();
575
574
  ctx.fillStyle = colors.tick;
576
- ctx.font = "9px system-ui, sans-serif";
575
+ ctx.font = `9px ${colors.fontMono}`;
577
576
  ctx.textAlign = "center";
578
577
  ctx.fillText(`⋯ ${formatDurationMs(gap.t1 - gap.t0)}`, x, RULER_H - 4);
579
578
  ctx.textAlign = "left";
580
579
  }
581
580
 
582
581
  // Ruler: major ticks get a full-height gridline, minors stay in the ruler.
583
- ctx.font = "9px system-ui, sans-serif";
582
+ ctx.font = `9px ${colors.fontMono}`;
584
583
  for (const tick of buildTicks(scale, u0, u1, width)) {
585
584
  const x = Math.round(toX(tick.u)) + 0.5;
586
585
  ctx.strokeStyle = colors.grid;
@@ -625,9 +624,11 @@ function draw(
625
624
  ctx.globalAlpha = matches ? 1 : 0.3;
626
625
  if (isTurnLane) {
627
626
  // Turn ranges read as context, not work: soft fill + solid start cap.
628
- ctx.fillStyle = `${color}3a`;
627
+ ctx.fillStyle = color;
628
+ ctx.globalAlpha = (matches ? 1 : 0.3) * 0.23;
629
629
  spanPath(ctx, x, y, w, LANE_H);
630
630
  ctx.fill();
631
+ ctx.globalAlpha = matches ? 1 : 0.3;
631
632
  ctx.fillStyle = color;
632
633
  ctx.fillRect(x, y, 2, LANE_H);
633
634
  } else {
@@ -636,7 +637,7 @@ function draw(
636
637
  ctx.fill();
637
638
  }
638
639
  if (span.isError) {
639
- ctx.fillStyle = `${colors.error}55`;
640
+ ctx.fillStyle = colors.errorSoft;
640
641
  spanPath(ctx, x, y, w, LANE_H);
641
642
  ctx.fill();
642
643
  ctx.fillStyle = colors.error;
@@ -677,7 +678,7 @@ function draw(
677
678
  ctx.rect(x + 3, y, w - 6, LANE_H);
678
679
  ctx.clip();
679
680
  ctx.fillStyle = isTurnLane ? colors.tick : colors.spanText;
680
- ctx.font = "10px system-ui, sans-serif";
681
+ ctx.font = `10px ${colors.fontSans}`;
681
682
  ctx.fillText(span.label, x + 5, y + LANE_H - 5);
682
683
  ctx.restore();
683
684
  }
@@ -713,28 +714,14 @@ function renderTooltip(hover: HoverState, traceStart: number) {
713
714
  // Fixed positioning escapes the scroll frame's clipping; clamp to the window.
714
715
  const left = Math.min(hover.clientX + 14, window.innerWidth - 320);
715
716
  const top = hover.clientY + 16 > window.innerHeight - 140 ? hover.clientY - 120 : hover.clientY + 16;
716
- const style: React.CSSProperties = {
717
- position: "fixed",
718
- left,
719
- top,
720
- zIndex: 60,
721
- maxWidth: 300,
722
- pointerEvents: "none",
723
- background: "var(--surface-2)",
724
- border: "1px solid var(--border-strong)",
725
- borderRadius: 6,
726
- padding: "7px 10px",
727
- fontSize: 11,
728
- lineHeight: 1.5,
729
- boxShadow: "0 1px 2px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.25)",
730
- };
717
+ const style: React.CSSProperties = { left, top };
731
718
 
732
719
  if (hover.hit.kind === "marker") {
733
720
  const { marker } = hover.hit;
734
721
  return (
735
- <div style={style}>
736
- <div className="stats-font-medium stats-text-primary">{marker.label}</div>
737
- <div className="stats-text-muted">
722
+ <div className="traces-tooltip" style={style}>
723
+ <div className="traces-tooltip-title">{marker.label}</div>
724
+ <div className="muted num">
738
725
  {new Date(marker.time).toLocaleTimeString()} ({formatOffset(marker.time - traceStart)})
739
726
  </div>
740
727
  </div>
@@ -743,26 +730,22 @@ function renderTooltip(hover: HoverState, traceStart: number) {
743
730
 
744
731
  const { span } = hover.hit;
745
732
  return (
746
- <div style={style}>
747
- <div className="stats-font-medium stats-text-primary truncate">{span.label}</div>
748
- <div className="stats-text-muted">
733
+ <div className="traces-tooltip" style={style}>
734
+ <div className="traces-tooltip-title truncate">{span.label}</div>
735
+ <div className="muted num">
749
736
  {formatDurationMs(span.end - span.start)} · {new Date(span.start).toLocaleTimeString()} (
750
737
  {formatOffset(span.start - traceStart)}){span.unterminated ? " · unterminated" : ""}
751
738
  </div>
752
739
  {span.kind === "model" && (
753
- <div className="stats-text-muted">
740
+ <div className="muted num">
754
741
  {span.tokens !== undefined && <>{span.tokens.toLocaleString()} tok</>}
755
742
  {span.cost !== undefined && <> · ${span.cost.toFixed(4)}</>}
756
743
  {span.ttft !== undefined && <> · TTFT {formatDurationMs(span.ttft)}</>}
757
744
  {span.isError && <> · error</>}
758
745
  </div>
759
746
  )}
760
- {span.kind === "subagent" && span.model && <div className="stats-text-muted">{span.model}</div>}
761
- {span.detail && (
762
- <div className="stats-text-secondary" style={{ wordBreak: "break-word" }}>
763
- {span.detail}
764
- </div>
765
- )}
747
+ {span.kind === "subagent" && span.model && <div className="muted num">{span.model}</div>}
748
+ {span.detail && <div className="traces-tooltip-detail">{span.detail}</div>}
766
749
  </div>
767
750
  );
768
751
  }
@@ -4,19 +4,19 @@
4
4
  * Owns the shared viewport/selection state so all pieces stay in sync.
5
5
  */
6
6
 
7
- import { ArrowLeft, RefreshCw } from "lucide-react";
7
+ import { ArrowLeft, ChevronLeft, ChevronRight, RefreshCw } from "lucide-react";
8
8
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
9
  import { getSessionTrace } from "../api";
10
- import { useResource } from "../data/useResource";
11
- import type { TraceSpan, TraceTrack } from "../types";
12
- import { AsyncBoundary } from "../ui/AsyncBoundary";
13
- import { SegmentedControl } from "../ui/SegmentedControl";
10
+ import { useQuery } from "../data/query";
11
+ import type { TraceSpan, TraceSpanKind, TraceTrack } from "../types";
12
+ import { Card, ChartSkeleton, PageHeader, QueryView, SearchInput, Segmented } from "../ui";
14
13
  import { AggregatesPanel } from "./AggregatesPanel";
15
14
  import { Minimap } from "./Minimap";
16
15
  import { SpanDrawer } from "./SpanDrawer";
17
16
  import { SummaryStrip } from "./SummaryStrip";
18
17
  import { TimelineCanvas, type TimelineViewport } from "./TimelineCanvas";
19
18
  import { TranscriptList } from "./TranscriptList";
19
+ import { CATEGORY_VARS } from "./trace-colors";
20
20
  import { type AxisMode, buildScale } from "./time-scale";
21
21
 
22
22
  export interface TraceViewProps {
@@ -31,17 +31,18 @@ const MODE_OPTIONS: Array<{ value: AxisMode; label: string; title?: string }> =
31
31
  { value: "calls", label: "Calls", title: "Equal width per model/tool call boundary" },
32
32
  ];
33
33
 
34
+ const LEGEND: Array<{ kind: TraceSpanKind; label: string }> = [
35
+ { kind: "turn", label: "Input" },
36
+ { kind: "model", label: "Model" },
37
+ { kind: "tool", label: "Tool" },
38
+ { kind: "subagent", label: "Agent" },
39
+ { kind: "background", label: "Background" },
40
+ ];
41
+
34
42
  export function TraceView({ file, active, onBack }: TraceViewProps) {
35
- const {
36
- data: trace,
37
- error,
38
- loading,
39
- refetch,
40
- refreshing,
41
- } = useResource(["trace", file], signal => getSessionTrace(file, signal), {
42
- pollMs: 15000,
43
- enabled: active,
44
- });
43
+ const query = useQuery(["trace", file], () => getSessionTrace(file), { pollMs: 15000, enabled: active });
44
+ // A previous session's trace must never render under this file's header.
45
+ const trace = query.stale ? null : query.data;
45
46
 
46
47
  const [mode, setMode] = useState<AxisMode>("time");
47
48
  const [compressIdle, setCompressIdle] = useState(true);
@@ -199,139 +200,183 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
199
200
 
200
201
  const traceStart = trace?.startedAt ?? 0;
201
202
 
203
+ const traceQuery = { ...query, data: trace };
204
+
202
205
  return (
203
- <div className="stats-trace-view">
204
- <div className="stats-trace-header">
205
- <button type="button" onClick={onBack} className="stats-trace-back">
206
- <ArrowLeft size={13} aria-hidden="true" />
206
+ <div className="stack traces-view">
207
+ <div>
208
+ <button type="button" className="btn" data-variant="ghost" data-size="sm" onClick={onBack}>
209
+ <ArrowLeft size={14} aria-hidden="true" />
207
210
  Sessions
208
211
  </button>
209
- <h2 className="stats-trace-title" style={{ maxWidth: 480, margin: 0 }}>
210
- {trace?.title ?? file.split("/").pop()}
211
- </h2>
212
- {trace?.cwd && <span className="stats-trace-cwd">{trace.cwd}</span>}
213
- <button
214
- type="button"
215
- onClick={() => void refetch()}
216
- className="stats-trace-icon-btn"
217
- aria-label="Refresh trace"
218
- title="Refresh trace"
219
- style={{ marginLeft: "auto" }}
220
- >
221
- <RefreshCw size={14} className={refreshing ? "stats-spin" : undefined} />
222
- </button>
223
212
  </div>
213
+ <PageHeader
214
+ title={<span className="traces-title truncate">{trace?.title ?? file.split("/").pop()}</span>}
215
+ description={trace?.cwd ? <span className="mono">{trace.cwd}</span> : undefined}
216
+ actions={
217
+ <button
218
+ type="button"
219
+ className="btn"
220
+ data-variant="ghost"
221
+ data-icon="true"
222
+ onClick={query.refetch}
223
+ aria-label="Refresh trace"
224
+ title="Refresh trace"
225
+ >
226
+ <RefreshCw size={14} className={query.refreshing ? "traces-spin" : undefined} />
227
+ </button>
228
+ }
229
+ />
224
230
 
225
- <AsyncBoundary loading={loading} error={error} data={trace}>
226
- {trace && (
231
+ <QueryView query={traceQuery} skeleton={<ChartSkeleton height={420} />}>
232
+ {loaded => (
227
233
  <>
228
- <SummaryStrip summary={trace.summary} />
234
+ <SummaryStrip summary={loaded.summary} />
229
235
 
230
- <div className="stats-trace-toolbar">
231
- <SegmentedControl options={MODE_OPTIONS} value={mode} onChange={setMode} />
232
- {mode === "time" && (
233
- <label className="stats-trace-check">
234
- <input
235
- type="checkbox"
236
- checked={compressIdle}
237
- onChange={event => setCompressIdle(event.target.checked)}
236
+ <Card
237
+ index={1}
238
+ title="Timeline"
239
+ description="W/S zoom · A/D pan · drag pan · wheel zoom · 0 fit · F focus selection · dbl-click focus · Esc deselect"
240
+ actions={
241
+ <>
242
+ <button
243
+ type="button"
244
+ className="btn"
245
+ data-variant="ghost"
246
+ data-size="sm"
247
+ onClick={() => collapseAll(false)}
248
+ >
249
+ Expand all
250
+ </button>
251
+ <button
252
+ type="button"
253
+ className="btn"
254
+ data-variant="ghost"
255
+ data-size="sm"
256
+ onClick={() => collapseAll(true)}
257
+ >
258
+ Collapse all
259
+ </button>
260
+ </>
261
+ }
262
+ >
263
+ <div className="stack">
264
+ <div className="traces-toolbar">
265
+ <Segmented
266
+ options={MODE_OPTIONS}
267
+ value={mode}
268
+ onChange={setMode}
269
+ size="sm"
270
+ aria-label="Axis mode"
238
271
  />
239
- Compress idle
240
- </label>
241
- )}
242
- <div style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
243
- <input
244
- type="search"
245
- value={search}
246
- onChange={event => {
247
- setSearch(event.target.value);
248
- setMatchIndex(0);
249
- }}
250
- placeholder="Search spans…"
251
- aria-label="Search spans"
252
- spellCheck={false}
253
- className="stats-trace-input"
254
- style={{ width: 190 }}
272
+ {mode === "time" && (
273
+ <label className="traces-check">
274
+ <input
275
+ type="checkbox"
276
+ checked={compressIdle}
277
+ onChange={event => setCompressIdle(event.target.checked)}
278
+ />
279
+ Compress idle
280
+ </label>
281
+ )}
282
+ <div className="traces-search">
283
+ <SearchInput
284
+ value={search}
285
+ onChange={value => {
286
+ setSearch(value);
287
+ setMatchIndex(0);
288
+ }}
289
+ placeholder="Search spans…"
290
+ width={200}
291
+ />
292
+ {search.trim() && (
293
+ <>
294
+ <span className="num muted traces-match-count">
295
+ {matches.length === 0
296
+ ? "0"
297
+ : `${(matchIndex % Math.max(matches.length, 1)) + 1}/${matches.length}`}
298
+ </span>
299
+ <button
300
+ type="button"
301
+ className="btn"
302
+ data-variant="ghost"
303
+ data-size="sm"
304
+ data-icon="true"
305
+ onClick={() => cycleMatch(-1)}
306
+ aria-label="Previous match"
307
+ disabled={matches.length === 0}
308
+ >
309
+ <ChevronLeft size={14} />
310
+ </button>
311
+ <button
312
+ type="button"
313
+ className="btn"
314
+ data-variant="ghost"
315
+ data-size="sm"
316
+ data-icon="true"
317
+ onClick={() => cycleMatch(1)}
318
+ aria-label="Next match"
319
+ disabled={matches.length === 0}
320
+ >
321
+ <ChevronRight size={14} />
322
+ </button>
323
+ </>
324
+ )}
325
+ </div>
326
+ <div className="traces-legend">
327
+ {LEGEND.map(item => (
328
+ <span key={item.kind} className="traces-legend-item">
329
+ <span
330
+ className="swatch"
331
+ style={{ background: `var(${CATEGORY_VARS[item.kind]})` }}
332
+ />
333
+ {item.label}
334
+ </span>
335
+ ))}
336
+ </div>
337
+ </div>
338
+ <Minimap
339
+ tracks={tracks}
340
+ scale={scale}
341
+ viewport={effectiveViewport}
342
+ onViewportChange={setViewport}
343
+ />
344
+ <TimelineCanvas
345
+ tracks={tracks}
346
+ scale={scale}
347
+ viewport={effectiveViewport}
348
+ onViewportChange={setViewport}
349
+ selection={selection}
350
+ onSelect={selectAndOpen}
351
+ search={search}
352
+ collapsed={collapsed}
353
+ onToggleCollapse={toggleCollapse}
354
+ traceStart={traceStart}
255
355
  />
256
- {search.trim() && (
257
- <>
258
- <span
259
- className="stats-text-muted"
260
- style={{ fontSize: 11, fontVariantNumeric: "tabular-nums" }}
261
- >
262
- {matches.length === 0
263
- ? "0"
264
- : `${(matchIndex % Math.max(matches.length, 1)) + 1}/${matches.length}`}
265
- </span>
266
- <button
267
- type="button"
268
- onClick={() => cycleMatch(-1)}
269
- className="stats-trace-icon-btn"
270
- aria-label="Previous match"
271
- disabled={matches.length === 0}
272
- >
273
- ‹
274
- </button>
275
- <button
276
- type="button"
277
- onClick={() => cycleMatch(1)}
278
- className="stats-trace-icon-btn"
279
- aria-label="Next match"
280
- disabled={matches.length === 0}
281
- >
282
- ›
283
- </button>
284
- </>
285
- )}
286
- </div>
287
- <div style={{ marginLeft: "auto", display: "inline-flex", gap: 6 }}>
288
- <button type="button" onClick={() => collapseAll(false)} className="stats-trace-back">
289
- Expand all
290
- </button>
291
- <button type="button" onClick={() => collapseAll(true)} className="stats-trace-back">
292
- Collapse all
293
- </button>
294
356
  </div>
295
- </div>
357
+ </Card>
296
358
 
297
- <div className="stats-trace-timeline-card">
298
- <Minimap
359
+ <Card
360
+ index={2}
361
+ title="Transcript"
362
+ description="Every span and marker in order; click to inspect"
363
+ flush
364
+ >
365
+ <TranscriptList
299
366
  tracks={tracks}
300
- scale={scale}
301
- viewport={effectiveViewport}
302
- onViewportChange={setViewport}
303
- />
304
- <TimelineCanvas
305
- tracks={tracks}
306
- scale={scale}
307
- viewport={effectiveViewport}
308
- onViewportChange={setViewport}
309
367
  selection={selection}
310
- onSelect={selectAndOpen}
368
+ onSelect={spanId => {
369
+ selectAndReveal(spanId);
370
+ setDrawerOpen(true);
371
+ }}
311
372
  search={search}
312
- collapsed={collapsed}
313
- onToggleCollapse={toggleCollapse}
314
373
  traceStart={traceStart}
315
374
  />
316
- <div className="stats-trace-toolbar-hint" style={{ marginLeft: 0 }}>
317
- W/S zoom · A/D pan · drag pan · wheel zoom · 0 fit · F focus selection · dbl-click focus · Esc
318
- deselect
319
- </div>
320
- </div>
321
- <TranscriptList
322
- tracks={tracks}
323
- selection={selection}
324
- onSelect={spanId => {
325
- selectAndReveal(spanId);
326
- setDrawerOpen(true);
327
- }}
328
- search={search}
329
- traceStart={traceStart}
330
- />
331
- <AggregatesPanel toolStats={trace.summary.toolStats} />
375
+ </Card>
376
+ <AggregatesPanel toolStats={loaded.summary.toolStats} index={3} />
332
377
  </>
333
378
  )}
334
- </AsyncBoundary>
379
+ </QueryView>
335
380
 
336
381
  <SpanDrawer
337
382
  span={drawerOpen ? (selected?.span ?? null) : null}
@@ -6,10 +6,10 @@
6
6
 
7
7
  import { useEffect, useMemo, useRef, useState } from "react";
8
8
  import { formatDurationMs } from "../data/formatters";
9
+ import { EmptyState } from "../ui";
9
10
  import type { TraceMarker, TraceSpan, TraceTrack } from "../types";
10
- import { useSystemTheme } from "../useSystemTheme";
11
11
  import { formatOffset } from "./time-scale";
12
- import { TRACE_THEMES } from "./trace-colors";
12
+ import { useTraceTheme } from "./trace-colors";
13
13
 
14
14
  export interface TranscriptRow {
15
15
  key: string;
@@ -50,8 +50,7 @@ export function buildTranscriptRows(tracks: TraceTrack[]): TranscriptRow[] {
50
50
  }
51
51
 
52
52
  export function TranscriptList({ tracks, selection, onSelect, search, traceStart }: TranscriptListProps) {
53
- const theme = useSystemTheme();
54
- const colors = TRACE_THEMES[theme];
53
+ const colors = useTraceTheme();
55
54
  const containerRef = useRef<HTMLDivElement>(null);
56
55
  const [scrollTop, setScrollTop] = useState(0);
57
56
 
@@ -83,11 +82,7 @@ export function TranscriptList({ tracks, selection, onSelect, search, traceStart
83
82
  const visible = rows.slice(firstVisible, lastVisible);
84
83
 
85
84
  return (
86
- <div
87
- ref={containerRef}
88
- onScroll={event => setScrollTop(event.currentTarget.scrollTop)}
89
- className="stats-trace-list"
90
- >
85
+ <div ref={containerRef} onScroll={event => setScrollTop(event.currentTarget.scrollTop)} className="traces-list">
91
86
  <div style={{ height: rows.length * ROW_H, position: "relative" }}>
92
87
  <div style={{ position: "absolute", top: firstVisible * ROW_H, left: 0, right: 0 }}>
93
88
  {visible.map(row => {
@@ -100,37 +95,18 @@ export function TranscriptList({ tracks, selection, onSelect, search, traceStart
100
95
  const chipText = row.span?.kind ?? row.marker?.kind ?? "";
101
96
  const rowContent = (
102
97
  <>
103
- <span
104
- className="stats-trace-kind"
105
- style={{
106
- color,
107
- background: `${color}1c`,
108
- }}
109
- >
98
+ <span className="traces-kind" style={{ "--traces-kind-color": color } as React.CSSProperties}>
110
99
  {chipText}
111
100
  </span>
112
101
  {row.depth > 0 && (
113
- <span
114
- className="stats-text-muted truncate"
115
- style={{ flexShrink: 0, maxWidth: 90, fontSize: 9 }}
116
- title={row.track.label}
117
- >
102
+ <span className="traces-row-track mono truncate" title={row.track.label}>
118
103
  {row.track.id}
119
104
  </span>
120
105
  )}
121
- <span
122
- className="stats-text-primary truncate"
123
- style={{ fontSize: 11, minWidth: 0, flexShrink: 1 }}
124
- >
125
- {label}
126
- </span>
127
- {detail && (
128
- <span className="stats-text-muted truncate" style={{ fontSize: 10, minWidth: 0, flex: 1 }}>
129
- {detail}
130
- </span>
131
- )}
132
- <span className="stats-trace-row-meta">
133
- {row.span?.isError && <span style={{ color: colors.error }}>error</span>}
106
+ <span className="traces-row-label truncate">{label}</span>
107
+ {detail && <span className="traces-row-detail truncate">{detail}</span>}
108
+ <span className="traces-row-meta num">
109
+ {row.span?.isError && <span className="tone-bad">error</span>}
134
110
  <span>{duration}</span>
135
111
  <span>{formatOffset(row.time - traceStart)}</span>
136
112
  </span>
@@ -142,25 +118,21 @@ export function TranscriptList({ tracks, selection, onSelect, search, traceStart
142
118
  key={row.key}
143
119
  type="button"
144
120
  onClick={() => onSelect(span.id)}
145
- className="stats-trace-row"
121
+ className="traces-row"
146
122
  data-selected={isSelected ? "true" : "false"}
147
123
  style={indent}
148
124
  >
149
125
  {rowContent}
150
126
  </button>
151
127
  ) : (
152
- <div key={row.key} className="stats-trace-row stats-trace-row-static" style={indent}>
128
+ <div key={row.key} className="traces-row" data-static="true" style={indent}>
153
129
  {rowContent}
154
130
  </div>
155
131
  );
156
132
  })}
157
133
  </div>
158
134
  </div>
159
- {rows.length === 0 && (
160
- <div className="stats-text-muted" style={{ padding: 16, fontSize: 12 }}>
161
- No matching events
162
- </div>
163
- )}
135
+ {rows.length === 0 && <EmptyState title="No matching events" />}
164
136
  </div>
165
137
  );
166
138
  }