@oh-my-pi/omp-stats 18.3.4 → 18.4.0
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.
- package/CHANGELOG.md +16 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +123 -40
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
|
@@ -1,88 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* time descending
|
|
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 {
|
|
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
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
<
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
258
|
-
</
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
)}
|
|
220
|
+
{loading && (
|
|
221
|
+
<div className="stack">
|
|
222
|
+
<Skeleton height={80} />
|
|
223
|
+
<Skeleton height={160} />
|
|
304
224
|
</div>
|
|
305
|
-
|
|
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
|
-
</
|
|
241
|
+
</Drawer>
|
|
308
242
|
);
|
|
309
243
|
}
|
|
@@ -1,37 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Headline
|
|
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,
|
|
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
|
|
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
|
-
<
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|