@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
|
@@ -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 {
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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="
|
|
737
|
-
<div className="
|
|
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="
|
|
748
|
-
<div className="
|
|
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="
|
|
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="
|
|
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 {
|
|
11
|
-
import type { TraceSpan, TraceTrack } from "../types";
|
|
12
|
-
import {
|
|
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
|
-
|
|
37
|
-
|
|
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="
|
|
204
|
-
<div
|
|
205
|
-
<button type="button"
|
|
206
|
-
<ArrowLeft size={
|
|
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
|
-
<
|
|
226
|
-
{
|
|
231
|
+
<QueryView query={traceQuery} skeleton={<ChartSkeleton height={420} />}>
|
|
232
|
+
{loaded => (
|
|
227
233
|
<>
|
|
228
|
-
<SummaryStrip summary={
|
|
234
|
+
<SummaryStrip summary={loaded.summary} />
|
|
229
235
|
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
</
|
|
357
|
+
</Card>
|
|
296
358
|
|
|
297
|
-
<
|
|
298
|
-
|
|
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={
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
</
|
|
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 {
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
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="
|
|
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="
|
|
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
|
}
|