@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
@@ -1,88 +1,69 @@
1
1
  /**
2
- * Collapsible per-tool duration aggregates for one trace, sorted by total
3
- * time descending (as produced by the server).
2
+ * Per-tool duration aggregates for one trace (sortable; server order is total
3
+ * time descending).
4
4
  */
5
5
 
6
- import { ChevronDown, ChevronRight } from "lucide-react";
7
- import { useMemo, useState } from "react";
8
6
  import { formatDurationMs, formatInteger } from "../data/formatters";
9
7
  import type { TraceToolStat } from "../types";
10
- import { DataTable } from "../ui/DataTable";
8
+ import { Badge, Card, type Column, MeterCell, Table } from "../ui";
11
9
 
12
10
  export interface AggregatesPanelProps {
13
11
  toolStats: TraceToolStat[];
12
+ index?: number;
14
13
  }
15
14
 
16
- export function AggregatesPanel({ toolStats }: AggregatesPanelProps) {
17
- const [open, setOpen] = useState(false);
18
-
19
- const columns = useMemo(
20
- () => [
21
- { key: "tool", header: "Tool", render: (item: TraceToolStat) => item.tool },
22
- { key: "calls", header: "Calls", numeric: true, render: (item: TraceToolStat) => formatInteger(item.calls) },
23
- {
24
- key: "errors",
25
- header: "Errors",
26
- numeric: true,
27
- render: (item: TraceToolStat) => formatInteger(item.errors),
28
- },
29
- {
30
- key: "total",
31
- header: "Total",
32
- numeric: true,
33
- render: (item: TraceToolStat) => formatDurationMs(item.totalMs),
34
- },
35
- {
36
- key: "avg",
37
- header: "Avg",
38
- numeric: true,
39
- render: (item: TraceToolStat) => formatDurationMs(item.calls > 0 ? item.totalMs / item.calls : 0),
40
- },
41
- { key: "max", header: "Max", numeric: true, render: (item: TraceToolStat) => formatDurationMs(item.maxMs) },
42
- ],
43
- [],
44
- );
45
-
15
+ export function AggregatesPanel({ toolStats, index }: AggregatesPanelProps) {
46
16
  if (toolStats.length === 0) return null;
47
-
48
- return (
49
- <div className="stats-panel">
50
- <button
51
- type="button"
52
- onClick={() => setOpen(prev => !prev)}
53
- aria-expanded={open}
54
- style={{
55
- display: "flex",
56
- alignItems: "center",
57
- gap: 6,
58
- width: "100%",
59
- background: "none",
60
- border: "none",
61
- padding: "10px 14px",
62
- cursor: "pointer",
63
- textAlign: "left",
64
- }}
65
- >
66
- {open ? (
67
- <ChevronDown size={14} className="stats-text-muted" aria-hidden="true" />
17
+ const maxTotal = Math.max(...toolStats.map(stat => stat.totalMs));
18
+ const columns: Column<TraceToolStat>[] = [
19
+ { key: "tool", header: "Tool", render: row => <span className="mono">{row.tool}</span>, sort: row => row.tool },
20
+ {
21
+ key: "calls",
22
+ header: "Calls",
23
+ align: "right",
24
+ render: row => <span className="num">{formatInteger(row.calls)}</span>,
25
+ sort: row => row.calls,
26
+ },
27
+ {
28
+ key: "errors",
29
+ header: "Errors",
30
+ align: "right",
31
+ render: row =>
32
+ row.errors > 0 ? (
33
+ <Badge tone="bad">{formatInteger(row.errors)} failed</Badge>
68
34
  ) : (
69
- <ChevronRight size={14} className="stats-text-muted" aria-hidden="true" />
70
- )}
71
- <span className="stats-panel-title">Tool Aggregates</span>
72
- <span className="stats-text-muted" style={{ fontSize: 11 }}>
73
- {toolStats.length} tools
74
- </span>
75
- </button>
76
- {open && (
77
- <div className="stats-panel-body">
78
- <DataTable
79
- columns={columns}
80
- data={toolStats}
81
- keyExtractor={item => item.tool}
82
- emptyText="No tool calls"
83
- />
84
- </div>
85
- )}
86
- </div>
35
+ <span className="num dim">0</span>
36
+ ),
37
+ sort: row => row.errors,
38
+ },
39
+ {
40
+ key: "total",
41
+ header: "Total",
42
+ align: "right",
43
+ width: 200,
44
+ render: row => (
45
+ <MeterCell value={row.totalMs} max={maxTotal} display={formatDurationMs(row.totalMs)} color="var(--warn)" />
46
+ ),
47
+ sort: row => row.totalMs,
48
+ },
49
+ {
50
+ key: "avg",
51
+ header: "Avg",
52
+ align: "right",
53
+ render: row => <span className="num">{formatDurationMs(row.calls > 0 ? row.totalMs / row.calls : 0)}</span>,
54
+ sort: row => (row.calls > 0 ? row.totalMs / row.calls : 0),
55
+ },
56
+ {
57
+ key: "max",
58
+ header: "Max",
59
+ align: "right",
60
+ render: row => <span className="num">{formatDurationMs(row.maxMs)}</span>,
61
+ sort: row => row.maxMs,
62
+ },
63
+ ];
64
+ return (
65
+ <Card title="Tool aggregates" description={`${toolStats.length} tools, by total time`} flush index={index}>
66
+ <Table columns={columns} rows={toolStats} rowKey={row => row.tool} dense limit={12} />
67
+ </Card>
87
68
  );
88
69
  }
@@ -5,10 +5,9 @@
5
5
 
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
7
  import type { TraceSpanKind, TraceTrack } from "../types";
8
- import { useSystemTheme } from "../useSystemTheme";
9
8
  import type { TimelineViewport } from "./TimelineCanvas";
10
9
  import type { TraceScale } from "./time-scale";
11
- import { TRACE_THEMES } from "./trace-colors";
10
+ import { useTraceTheme } from "./trace-colors";
12
11
 
13
12
  export interface MinimapProps {
14
13
  tracks: TraceTrack[];
@@ -25,8 +24,7 @@ const MIN_WINDOW_U = 10;
25
24
  type DragMode = "move" | "left" | "right" | "create";
26
25
 
27
26
  export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapProps) {
28
- const theme = useSystemTheme();
29
- const colors = TRACE_THEMES[theme];
27
+ const colors = useTraceTheme();
30
28
  const canvasRef = useRef<HTMLCanvasElement>(null);
31
29
  const [width, setWidth] = useState(800);
32
30
  const dragRef = useRef<{ mode: DragMode; startX: number; startViewport: TimelineViewport } | null>(null);
@@ -80,14 +78,14 @@ export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapPr
80
78
  // Viewport brush.
81
79
  const vx0 = toX(viewport.u0);
82
80
  const vx1 = toX(viewport.u1);
83
- ctx.fillStyle = theme === "dark" ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
81
+ ctx.fillStyle = colors.brush;
84
82
  ctx.fillRect(vx0, 0, vx1 - vx0, HEIGHT);
85
83
  ctx.strokeStyle = colors.selection;
86
84
  ctx.strokeRect(vx0 + 0.5, 0.5, vx1 - vx0 - 1, HEIGHT - 1);
87
85
  ctx.fillStyle = colors.selection;
88
86
  ctx.fillRect(vx0, 0, 2, HEIGHT);
89
87
  ctx.fillRect(vx1 - 2, 0, 2, HEIGHT);
90
- }, [tracks, scale, viewport, colors, theme, width, toX]);
88
+ }, [tracks, scale, viewport, colors, width, toX]);
91
89
 
92
90
  const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {
93
91
  const rect = event.currentTarget.getBoundingClientRect();
@@ -145,15 +143,8 @@ export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapPr
145
143
  ref={canvasRef}
146
144
  width={Math.floor(width * devicePixelRatio)}
147
145
  height={Math.floor(HEIGHT * devicePixelRatio)}
148
- style={{
149
- width: "100%",
150
- height: HEIGHT,
151
- display: "block",
152
- cursor: "crosshair",
153
- borderRadius: 4,
154
- border: "1px solid var(--border)",
155
- touchAction: "none",
156
- }}
146
+ className="traces-minimap"
147
+ style={{ height: HEIGHT }}
157
148
  role="slider"
158
149
  aria-label="Timeline overview brush"
159
150
  aria-valuemin={0}
@@ -4,14 +4,12 @@
4
4
  * + text blocks, tool args/result, user text, subagent task text.
5
5
  */
6
6
 
7
- import { Check, Copy, X } from "lucide-react";
8
- import { useEffect, useRef, useState } from "react";
7
+ import { Check, Copy } from "lucide-react";
8
+ import { type ReactNode, useEffect, useRef, useState } from "react";
9
9
  import { getSessionEntryDetail } from "../api";
10
10
  import { formatDurationMs, formatInteger } from "../data/formatters";
11
11
  import type { TraceSpan, TraceTrack } from "../types";
12
- import { JsonBlock } from "../ui/JsonBlock";
13
- import { Skeleton } from "../ui/Skeleton";
14
- import { StatusPill } from "../ui/StatusPill";
12
+ import { Badge, Drawer, ErrorState, JsonBlock, KeyValues, Skeleton } from "../ui";
15
13
 
16
14
  export interface SpanDrawerProps {
17
15
  span: TraceSpan | null;
@@ -67,22 +65,19 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
67
65
  const [loading, setLoading] = useState(false);
68
66
  const [error, setError] = useState<Error | null>(null);
69
67
  const [copied, setCopied] = useState(false);
70
- const closeButtonRef = useRef<HTMLButtonElement>(null);
71
- const previousActiveElement = useRef<HTMLElement | null>(null);
68
+ const copyResetRef = useRef<number>(0);
72
69
 
73
70
  useEffect(() => {
74
71
  setEntry(null);
75
72
  setError(null);
76
73
  setCopied(false);
74
+ setLoading(false);
77
75
  if (!span || !track || !span.entryId) return;
78
- previousActiveElement.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
79
76
  setLoading(true);
80
77
  const controller = new AbortController();
81
78
  getSessionEntryDetail(track.file, span.entryId, controller.signal)
82
79
  .then(data => {
83
- if (controller.signal.aborted) return;
84
- setEntry(data.entry);
85
- setTimeout(() => closeButtonRef.current?.focus(), 50);
80
+ if (!controller.signal.aborted) setEntry(data.entry);
86
81
  })
87
82
  .catch(err => {
88
83
  if (controller.signal.aborted) return;
@@ -94,29 +89,16 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
94
89
  return () => controller.abort();
95
90
  }, [span, track]);
96
91
 
97
- useEffect(() => {
98
- if (!span) return;
99
- const handleKeyDown = (event: KeyboardEvent) => {
100
- if (event.key === "Escape") onClose();
101
- };
102
- window.addEventListener("keydown", handleKeyDown);
103
- return () => {
104
- window.removeEventListener("keydown", handleKeyDown);
105
- previousActiveElement.current?.focus();
106
- };
107
- }, [span, onClose]);
92
+ useEffect(() => () => window.clearTimeout(copyResetRef.current), []);
108
93
 
109
94
  if (!span) return null;
110
95
 
111
- const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
112
- if (event.target === event.currentTarget) onClose();
113
- };
114
-
115
96
  const handleCopy = async () => {
116
97
  try {
117
98
  await navigator.clipboard.writeText(JSON.stringify(entry ?? span, null, 2));
118
99
  setCopied(true);
119
- setTimeout(() => setCopied(false), 1500);
100
+ window.clearTimeout(copyResetRef.current);
101
+ copyResetRef.current = window.setTimeout(() => setCopied(false), 1500);
120
102
  } catch {
121
103
  // Clipboard unavailable (permissions); leave the button state as-is.
122
104
  }
@@ -127,183 +109,135 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
127
109
  const msg = view.message;
128
110
  const usage = msg?.usage;
129
111
 
112
+ const metrics: Array<{ key: string; label: ReactNode; value: ReactNode }> = [
113
+ {
114
+ key: "duration",
115
+ label: "Duration",
116
+ value: (
117
+ <span className="num">
118
+ {formatDurationMs(span.end - span.start)}
119
+ {span.unterminated && <span className="muted"> · unterminated</span>}
120
+ </span>
121
+ ),
122
+ },
123
+ { key: "start", label: "Start", value: <span className="num">{new Date(span.start).toLocaleTimeString()}</span> },
124
+ { key: "track", label: "Track", value: <span className="mono">{track?.label ?? "-"}</span> },
125
+ ];
126
+ if (span.kind === "model") {
127
+ metrics.push(
128
+ {
129
+ key: "tokens",
130
+ label: "Tokens",
131
+ value: <span className="num">{formatInteger(span.tokens ?? 0)}</span>,
132
+ },
133
+ { key: "cost", label: "Cost", value: <span className="num">${(span.cost ?? 0).toFixed(4)}</span> },
134
+ { key: "ttft", label: "TTFT", value: <span className="num">{formatDurationMs(span.ttft ?? null)}</span> },
135
+ { key: "model", label: "Model", value: <span className="mono">{msg?.model ?? span.model ?? "-"}</span> },
136
+ );
137
+ if (usage)
138
+ metrics.push({
139
+ key: "split",
140
+ label: "Input / output / cached",
141
+ value: (
142
+ <span className="num">
143
+ {formatInteger(usage.input ?? 0)} / {formatInteger(usage.output ?? 0)} /{" "}
144
+ {formatInteger(usage.cacheRead ?? 0)}
145
+ </span>
146
+ ),
147
+ });
148
+ if (msg?.provider)
149
+ metrics.push({ key: "provider", label: "Provider", value: <span className="mono">{msg.provider}</span> });
150
+ if (msg?.stopReason)
151
+ metrics.push({ key: "stop", label: "Stop reason", value: <span className="mono">{msg.stopReason}</span> });
152
+ }
153
+
130
154
  return (
131
- <div className="stats-drawer-overlay" onClick={handleOverlayClick} role="presentation">
132
- <div className="stats-drawer" role="dialog" aria-modal="true" aria-label="Span details">
133
- <div className="stats-drawer-header">
134
- <div className="stats-drawer-header-left">
135
- <h2 className="stats-drawer-title truncate" style={{ maxWidth: 320 }}>
136
- {span.label}
137
- </h2>
138
- <StatusPill variant={span.isError ? "danger" : "success"}>{span.kind}</StatusPill>
155
+ <Drawer
156
+ open
157
+ onClose={onClose}
158
+ title={span.label}
159
+ subtitle={
160
+ <span className="row" style={{ gap: 6 }}>
161
+ <Badge tone={span.isError ? "bad" : "neutral"}>
162
+ {span.isError ? `${span.kind} · error` : span.kind}
163
+ </Badge>
164
+ </span>
165
+ }
166
+ actions={
167
+ <button
168
+ type="button"
169
+ className="btn"
170
+ data-variant="ghost"
171
+ data-icon="true"
172
+ onClick={handleCopy}
173
+ aria-label="Copy raw JSON"
174
+ title="Copy raw JSON"
175
+ >
176
+ {copied ? <Check size={15} /> : <Copy size={15} />}
177
+ </button>
178
+ }
179
+ >
180
+ <div className="stack traces-drawer">
181
+ <KeyValues items={metrics} />
182
+
183
+ {msg?.errorMessage && (
184
+ <div className="stack" style={{ gap: 6 }}>
185
+ <div className="section-label tone-bad">Error message</div>
186
+ <pre className="code-block traces-pre">{msg.errorMessage}</pre>
139
187
  </div>
140
- <div style={{ display: "flex", gap: 8 }}>
141
- <button
142
- type="button"
143
- onClick={handleCopy}
144
- className="stats-drawer-close-btn"
145
- aria-label="Copy raw JSON"
146
- title="Copy raw JSON"
147
- >
148
- {copied ? <Check size={16} /> : <Copy size={16} />}
149
- </button>
150
- <button
151
- ref={closeButtonRef}
152
- type="button"
153
- onClick={onClose}
154
- className="stats-drawer-close-btn"
155
- aria-label="Close span details"
156
- >
157
- <X size={18} />
158
- </button>
159
- </div>
160
- </div>
161
-
162
- <div className="stats-drawer-body">
163
- <div className="stats-drawer-content">
164
- <div className="stats-drawer-metrics-grid">
165
- <div className="stats-drawer-metric-card">
166
- <div className="stats-drawer-metric-label">Duration</div>
167
- <div className="stats-drawer-metric-value">{formatDurationMs(span.end - span.start)}</div>
168
- {span.unterminated && <div className="stats-drawer-metric-sub">unterminated</div>}
169
- </div>
170
- <div className="stats-drawer-metric-card">
171
- <div className="stats-drawer-metric-label">Start</div>
172
- <div className="stats-drawer-metric-value" style={{ fontSize: 13 }}>
173
- {new Date(span.start).toLocaleTimeString()}
174
- </div>
175
- </div>
176
- {span.kind === "model" && (
177
- <>
178
- <div className="stats-drawer-metric-card">
179
- <div className="stats-drawer-metric-label">Tokens</div>
180
- <div className="stats-drawer-metric-value">{formatInteger(span.tokens ?? 0)}</div>
181
- {usage && (
182
- <div className="stats-drawer-metric-sub">
183
- {formatInteger(usage.input ?? 0)} in · {formatInteger(usage.output ?? 0)} out ·{" "}
184
- {formatInteger(usage.cacheRead ?? 0)} cached
185
- </div>
186
- )}
187
- </div>
188
- <div className="stats-drawer-metric-card">
189
- <div className="stats-drawer-metric-label">Cost</div>
190
- <div className="stats-drawer-metric-value">${(span.cost ?? 0).toFixed(4)}</div>
191
- </div>
192
- <div className="stats-drawer-metric-card">
193
- <div className="stats-drawer-metric-label">TTFT</div>
194
- <div className="stats-drawer-metric-value">{formatDurationMs(span.ttft ?? null)}</div>
195
- </div>
196
- <div className="stats-drawer-metric-card">
197
- <div className="stats-drawer-metric-label">Model</div>
198
- <div className="stats-drawer-metric-value" style={{ fontSize: 12 }}>
199
- {msg?.model ?? span.model ?? "-"}
200
- </div>
201
- {msg?.provider && <div className="stats-drawer-metric-sub">{msg.provider}</div>}
202
- {msg?.stopReason && <div className="stats-drawer-metric-sub">stop: {msg.stopReason}</div>}
203
- </div>
204
- </>
205
- )}
206
- </div>
207
-
208
- {msg?.errorMessage && (
209
- <div className="stats-drawer-error-block">
210
- <div className="stats-drawer-error-label">Error Message</div>
211
- <div className="stats-drawer-error-text">{msg.errorMessage}</div>
212
- </div>
188
+ )}
189
+
190
+ {span.kind === "subagent" && (
191
+ <div className="stack" style={{ gap: 8 }}>
192
+ {span.detail && (
193
+ <>
194
+ <div className="section-label">Task</div>
195
+ <pre className="code-block traces-pre">{span.detail}</pre>
196
+ </>
213
197
  )}
214
-
215
- {span.kind === "subagent" && (
198
+ {span.childTrackId && onOpenChildTrack && (
216
199
  <div>
217
- {span.detail && (
218
- <pre
219
- style={{
220
- whiteSpace: "pre-wrap",
221
- wordBreak: "break-word",
222
- fontSize: 12,
223
- background: "var(--surface-2)",
224
- borderRadius: 6,
225
- padding: 10,
226
- }}
227
- >
228
- {span.detail}
229
- </pre>
230
- )}
231
- {span.childTrackId && onOpenChildTrack && (
232
- <button
233
- type="button"
234
- className="stats-segmented-control-btn"
235
- style={{ marginTop: 8, border: "1px solid var(--border)", borderRadius: 6 }}
236
- onClick={() => onOpenChildTrack(span.childTrackId ?? "")}
237
- >
238
- Open child track {span.childTrackId}
239
- </button>
240
- )}
241
- </div>
242
- )}
243
-
244
- {span.kind === "tool" && span.detail && (
245
- <div>
246
- <div className="stats-drawer-metric-label">Args</div>
247
- <pre
248
- style={{
249
- whiteSpace: "pre-wrap",
250
- wordBreak: "break-word",
251
- fontSize: 12,
252
- background: "var(--surface-2)",
253
- borderRadius: 6,
254
- padding: 10,
255
- }}
200
+ <button
201
+ type="button"
202
+ className="btn"
203
+ data-size="sm"
204
+ onClick={() => onOpenChildTrack(span.childTrackId ?? "")}
256
205
  >
257
- {span.detail}
258
- </pre>
259
- </div>
260
- )}
261
-
262
- {loading && (
263
- <div>
264
- <Skeleton variant="rect" width="100%" height={80} className="mb-4" />
265
- <Skeleton variant="rect" width="100%" height={160} />
266
- </div>
267
- )}
268
- {error && (
269
- <div className="stats-drawer-error">
270
- <p className="stats-drawer-error-title">Failed to load entry</p>
271
- <p className="stats-drawer-error-message">{error.message}</p>
206
+ Open child track <span className="mono">{span.childTrackId}</span>
207
+ </button>
272
208
  </div>
273
209
  )}
210
+ </div>
211
+ )}
274
212
 
275
- {!loading &&
276
- entry !== null &&
277
- textBlocks(msg?.content).map((block, index) => (
278
- <div key={`${block.kind}-${index}`}>
279
- <div className="stats-drawer-metric-label" style={{ textTransform: "capitalize" }}>
280
- {block.kind}
281
- </div>
282
- <pre
283
- style={{
284
- whiteSpace: "pre-wrap",
285
- wordBreak: "break-word",
286
- fontSize: 12,
287
- background: "var(--surface-2)",
288
- borderRadius: 6,
289
- padding: 10,
290
- maxHeight: 360,
291
- overflowY: "auto",
292
- }}
293
- >
294
- {block.text}
295
- </pre>
296
- </div>
297
- ))}
213
+ {span.kind === "tool" && span.detail && (
214
+ <div className="stack" style={{ gap: 6 }}>
215
+ <div className="section-label">Args</div>
216
+ <pre className="code-block traces-pre">{span.detail}</pre>
217
+ </div>
218
+ )}
298
219
 
299
- {!loading && entry !== null && (
300
- <div className="stats-drawer-json-blocks">
301
- <JsonBlock data={entry} title="Raw Entry" initialCollapsed={true} />
302
- </div>
303
- )}
220
+ {loading && (
221
+ <div className="stack">
222
+ <Skeleton height={80} />
223
+ <Skeleton height={160} />
304
224
  </div>
305
- </div>
225
+ )}
226
+ {error && <ErrorState error={`Failed to load entry: ${error.message}`} />}
227
+
228
+ {!loading &&
229
+ entry !== null &&
230
+ textBlocks(msg?.content).map((block, index) => (
231
+ <div key={`${block.kind}-${index}`} className="stack" style={{ gap: 6 }}>
232
+ <div className="section-label">{block.kind === "thinking" ? "Thinking" : "Text"}</div>
233
+ <pre className="code-block traces-pre" data-scroll="true">
234
+ {block.text}
235
+ </pre>
236
+ </div>
237
+ ))}
238
+
239
+ {!loading && entry !== null && <JsonBlock data={entry} title="Raw entry" initialCollapsed={true} />}
306
240
  </div>
307
- </div>
241
+ </Drawer>
308
242
  );
309
243
  }
@@ -1,37 +1,34 @@
1
1
  /**
2
- * Headline chips for one trace: wall/model/tool/idle time, turns, requests,
3
- * tool calls, agents, tokens, cost.
2
+ * Headline tiles for one trace: wall time split into model/tool/idle, then
3
+ * turns, requests, tool calls, agents, tokens, cost.
4
4
  */
5
5
 
6
- import { formatCompact, formatDurationMs, formatEstimatedCost, formatInteger } from "../data/formatters";
6
+ import { formatCompact, formatElapsed, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
7
7
  import type { TraceSummary } from "../types";
8
+ import { Stat, StatGrid } from "../ui";
8
9
 
9
10
  export interface SummaryStripProps {
10
11
  summary: TraceSummary;
11
12
  }
12
13
 
13
14
  export function SummaryStrip({ summary }: SummaryStripProps) {
14
- const chips: Array<{ label: string; value: string }> = [
15
- { label: "Wall", value: formatDurationMs(summary.wallMs) },
16
- { label: "Model", value: formatDurationMs(summary.modelMs) },
17
- { label: "Tools", value: formatDurationMs(summary.toolMs) },
18
- { label: "Idle", value: formatDurationMs(summary.idleMs) },
19
- { label: "Turns", value: formatInteger(summary.turns) },
20
- { label: "Requests", value: formatInteger(summary.requests) },
21
- { label: "Tool Calls", value: formatInteger(summary.toolCalls) },
22
- { label: "Agents", value: formatInteger(summary.subagents) },
23
- { label: "Tokens", value: formatCompact(summary.totalTokens) },
24
- { label: "Cost", value: formatEstimatedCost(summary.costTotal, summary.unpricedRequests) },
25
- ];
26
-
15
+ const share = (ms: number) => (summary.wallMs > 0 ? `${formatPercent(ms / summary.wallMs)} of wall` : undefined);
27
16
  return (
28
- <div className="stats-trace-summary">
29
- {chips.map(chip => (
30
- <div key={chip.label} className="stats-trace-summary-cell">
31
- <span className="stats-trace-summary-label">{chip.label}</span>
32
- <span className="stats-trace-summary-value">{chip.value}</span>
33
- </div>
34
- ))}
35
- </div>
17
+ <StatGrid min={112}>
18
+ <Stat size="sm" label="Wall time" value={formatElapsed(summary.wallMs)} />
19
+ <Stat size="sm" label="Model time" value={formatElapsed(summary.modelMs)} hint={share(summary.modelMs)} />
20
+ <Stat size="sm" label="Tool time" value={formatElapsed(summary.toolMs)} hint={share(summary.toolMs)} />
21
+ <Stat size="sm" label="Idle" value={formatElapsed(summary.idleMs)} hint={share(summary.idleMs)} />
22
+ <Stat size="sm" label="Turns" value={formatInteger(summary.turns)} />
23
+ <Stat
24
+ size="sm"
25
+ label="Requests"
26
+ value={formatInteger(summary.requests)}
27
+ hint={`${formatInteger(summary.toolCalls)} tool calls`}
28
+ />
29
+ <Stat size="sm" label="Agents" value={formatInteger(summary.subagents)} />
30
+ <Stat size="sm" label="Tokens" value={formatCompact(summary.totalTokens)} />
31
+ <Stat size="sm" label="Cost" value={formatEstimatedCost(summary.costTotal, summary.unpricedRequests)} />
32
+ </StatGrid>
36
33
  );
37
34
  }