@oh-my-pi-zen/omp-stats 16.3.6-zen.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 (120) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +82 -0
  3. package/build.ts +95 -0
  4. package/dist/client/index.css +1 -0
  5. package/dist/client/index.html +24 -0
  6. package/dist/client/index.js +257 -0
  7. package/dist/client/styles.css +1656 -0
  8. package/dist/types/aggregator.d.ts +87 -0
  9. package/dist/types/client/App.d.ts +1 -0
  10. package/dist/types/client/api.d.ts +21 -0
  11. package/dist/types/client/app/AppLayout.d.ts +16 -0
  12. package/dist/types/client/app/NavRail.d.ts +7 -0
  13. package/dist/types/client/app/RangeControl.d.ts +7 -0
  14. package/dist/types/client/app/SyncButton.d.ts +14 -0
  15. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  16. package/dist/types/client/app/TopBar.d.ts +15 -0
  17. package/dist/types/client/app/routes.d.ts +12 -0
  18. package/dist/types/client/components/AgentTokenShare.d.ts +5 -0
  19. package/dist/types/client/components/chart-shared.d.ts +173 -0
  20. package/dist/types/client/components/models-table-shared.d.ts +175 -0
  21. package/dist/types/client/components/range-meta.d.ts +21 -0
  22. package/dist/types/client/data/charts.d.ts +1 -0
  23. package/dist/types/client/data/formatters.d.ts +8 -0
  24. package/dist/types/client/data/useHashRoute.d.ts +8 -0
  25. package/dist/types/client/data/useResource.d.ts +13 -0
  26. package/dist/types/client/data/view-models.d.ts +67 -0
  27. package/dist/types/client/index.d.ts +2 -0
  28. package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
  29. package/dist/types/client/routes/CostsRoute.d.ts +7 -0
  30. package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
  31. package/dist/types/client/routes/GainRoute.d.ts +7 -0
  32. package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
  33. package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
  34. package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
  35. package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
  36. package/dist/types/client/routes/ToolsRoute.d.ts +7 -0
  37. package/dist/types/client/routes/index.d.ts +9 -0
  38. package/dist/types/client/types.d.ts +63 -0
  39. package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
  40. package/dist/types/client/ui/DataTable.d.ts +17 -0
  41. package/dist/types/client/ui/EmptyState.d.ts +7 -0
  42. package/dist/types/client/ui/ErrorState.d.ts +6 -0
  43. package/dist/types/client/ui/JsonBlock.d.ts +7 -0
  44. package/dist/types/client/ui/MetricCluster.d.ts +5 -0
  45. package/dist/types/client/ui/Panel.d.ts +7 -0
  46. package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
  47. package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
  48. package/dist/types/client/ui/Skeleton.d.ts +8 -0
  49. package/dist/types/client/ui/StatusPill.d.ts +7 -0
  50. package/dist/types/client/ui/index.d.ts +11 -0
  51. package/dist/types/client/useSystemTheme.d.ts +11 -0
  52. package/dist/types/db.d.ts +144 -0
  53. package/dist/types/embedded-client.d.ts +18 -0
  54. package/dist/types/gain-aggregator.d.ts +26 -0
  55. package/dist/types/index.d.ts +7 -0
  56. package/dist/types/parser.d.ts +53 -0
  57. package/dist/types/server.d.ts +7 -0
  58. package/dist/types/shared-types.d.ts +301 -0
  59. package/dist/types/sync-worker.d.ts +31 -0
  60. package/dist/types/types.d.ts +164 -0
  61. package/dist/types/user-metrics.d.ts +72 -0
  62. package/package.json +95 -0
  63. package/src/aggregator.ts +501 -0
  64. package/src/client/App.tsx +109 -0
  65. package/src/client/api.ts +102 -0
  66. package/src/client/app/AppLayout.tsx +93 -0
  67. package/src/client/app/NavRail.tsx +44 -0
  68. package/src/client/app/RangeControl.tsx +39 -0
  69. package/src/client/app/SyncButton.tsx +75 -0
  70. package/src/client/app/ThemeToggle.tsx +37 -0
  71. package/src/client/app/TopBar.tsx +73 -0
  72. package/src/client/app/routes.ts +69 -0
  73. package/src/client/components/AgentTokenShare.tsx +68 -0
  74. package/src/client/components/chart-shared.tsx +257 -0
  75. package/src/client/components/models-table-shared.tsx +255 -0
  76. package/src/client/components/range-meta.ts +73 -0
  77. package/src/client/css.d.ts +1 -0
  78. package/src/client/data/charts.ts +14 -0
  79. package/src/client/data/formatters.ts +45 -0
  80. package/src/client/data/useHashRoute.ts +87 -0
  81. package/src/client/data/useResource.ts +154 -0
  82. package/src/client/data/view-models.ts +251 -0
  83. package/src/client/index.tsx +10 -0
  84. package/src/client/routes/BehaviorRoute.tsx +623 -0
  85. package/src/client/routes/CostsRoute.tsx +234 -0
  86. package/src/client/routes/ErrorsRoute.tsx +118 -0
  87. package/src/client/routes/GainRoute.tsx +226 -0
  88. package/src/client/routes/ModelsRoute.tsx +430 -0
  89. package/src/client/routes/OverviewRoute.tsx +342 -0
  90. package/src/client/routes/ProjectsRoute.tsx +163 -0
  91. package/src/client/routes/RequestsRoute.tsx +123 -0
  92. package/src/client/routes/ToolsRoute.tsx +463 -0
  93. package/src/client/routes/index.ts +9 -0
  94. package/src/client/styles.css +1330 -0
  95. package/src/client/types.ts +80 -0
  96. package/src/client/ui/AsyncBoundary.tsx +54 -0
  97. package/src/client/ui/DataTable.tsx +122 -0
  98. package/src/client/ui/EmptyState.tsx +16 -0
  99. package/src/client/ui/ErrorState.tsx +25 -0
  100. package/src/client/ui/JsonBlock.tsx +75 -0
  101. package/src/client/ui/MetricCluster.tsx +67 -0
  102. package/src/client/ui/Panel.tsx +24 -0
  103. package/src/client/ui/RequestDrawer.tsx +208 -0
  104. package/src/client/ui/SegmentedControl.tsx +36 -0
  105. package/src/client/ui/Skeleton.tsx +17 -0
  106. package/src/client/ui/StatusPill.tsx +15 -0
  107. package/src/client/ui/index.ts +11 -0
  108. package/src/client/useSystemTheme.ts +87 -0
  109. package/src/db.ts +1530 -0
  110. package/src/embedded-client.generated.txt +0 -0
  111. package/src/embedded-client.ts +26 -0
  112. package/src/gain-aggregator.ts +281 -0
  113. package/src/index.ts +194 -0
  114. package/src/parser.ts +450 -0
  115. package/src/server.ts +352 -0
  116. package/src/shared-types.ts +323 -0
  117. package/src/sync-worker.ts +40 -0
  118. package/src/types.ts +171 -0
  119. package/src/user-metrics.ts +685 -0
  120. package/tailwind.config.js +40 -0
@@ -0,0 +1,93 @@
1
+ import { X } from "lucide-react";
2
+ import type React from "react";
3
+ import { useState } from "react";
4
+ import type { TimeRange } from "../types";
5
+ import { NavRail } from "./NavRail";
6
+ import type { DashboardSection } from "./routes";
7
+ import { TopBar } from "./TopBar";
8
+
9
+ export interface AppLayoutProps {
10
+ activeSection: DashboardSection;
11
+ onSectionChange: (section: DashboardSection) => void;
12
+ range: TimeRange;
13
+ onRangeChange: (range: TimeRange) => void;
14
+ updatedAt: number | null;
15
+ onSyncStart?: () => void;
16
+ onSyncComplete?: (result: { success: boolean }) => void;
17
+ children: React.ReactNode;
18
+ }
19
+
20
+ export function AppLayout({
21
+ activeSection,
22
+ onSectionChange,
23
+ range,
24
+ onRangeChange,
25
+ updatedAt,
26
+ onSyncStart,
27
+ onSyncComplete,
28
+ children,
29
+ }: AppLayoutProps) {
30
+ const [menuOpen, setMenuOpen] = useState(false);
31
+
32
+ const handleSectionChange = (section: DashboardSection) => {
33
+ onSectionChange(section);
34
+ setMenuOpen(false);
35
+ };
36
+
37
+ return (
38
+ <div className="stats-app-container">
39
+ {/* Desktop Rail */}
40
+ <NavRail activeSection={activeSection} onSectionChange={handleSectionChange} className="stats-desktop-nav" />
41
+
42
+ {/* Mobile Nav Drawer */}
43
+ {menuOpen && (
44
+ <div className="stats-mobile-drawer-overlay" onClick={() => setMenuOpen(false)} role="presentation">
45
+ <div
46
+ className="stats-mobile-drawer"
47
+ onClick={e => e.stopPropagation()}
48
+ role="dialog"
49
+ aria-modal="true"
50
+ aria-label="Navigation menu"
51
+ >
52
+ <div className="stats-mobile-drawer-header">
53
+ <div className="stats-logo-container">
54
+ <span className="stats-logo-text">OH MY PI</span>
55
+ <span className="stats-logo-subtext">Observability</span>
56
+ </div>
57
+ <button
58
+ type="button"
59
+ onClick={() => setMenuOpen(false)}
60
+ className="stats-drawer-close-btn"
61
+ aria-label="Close navigation menu"
62
+ >
63
+ <X size={18} />
64
+ </button>
65
+ </div>
66
+ <NavRail
67
+ activeSection={activeSection}
68
+ onSectionChange={handleSectionChange}
69
+ className="stats-mobile-nav"
70
+ />
71
+ </div>
72
+ </div>
73
+ )}
74
+
75
+ {/* Main Layout Pane */}
76
+ <div className="stats-main-pane">
77
+ <TopBar
78
+ activeSection={activeSection}
79
+ range={range}
80
+ onRangeChange={onRangeChange}
81
+ updatedAt={updatedAt}
82
+ onSyncStart={onSyncStart}
83
+ onSyncComplete={onSyncComplete}
84
+ onMenuToggle={() => setMenuOpen(true)}
85
+ />
86
+
87
+ <main className="stats-content-area">
88
+ <div className="stats-content-inner">{children}</div>
89
+ </main>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,44 @@
1
+ import { type DashboardSection, routes } from "./routes";
2
+
3
+ export interface NavRailProps {
4
+ activeSection: DashboardSection;
5
+ onSectionChange: (section: DashboardSection) => void;
6
+ className?: string;
7
+ }
8
+
9
+ export function NavRail({ activeSection, onSectionChange, className = "" }: NavRailProps) {
10
+ return (
11
+ <aside className={`stats-nav-rail ${className}`}>
12
+ <div className="stats-nav-rail-header">
13
+ <div className="stats-logo-container">
14
+ <span className="stats-logo-text">OH MY PI</span>
15
+ <span className="stats-logo-subtext">Observability</span>
16
+ </div>
17
+ </div>
18
+
19
+ <nav className="stats-nav-rail-menu">
20
+ {routes.map(route => {
21
+ const isActive = route.id === activeSection;
22
+ const Icon = route.icon;
23
+ return (
24
+ <button
25
+ key={route.id}
26
+ type="button"
27
+ onClick={() => onSectionChange(route.id)}
28
+ className="stats-nav-rail-item"
29
+ data-active={isActive ? "true" : "false"}
30
+ aria-current={isActive ? "page" : undefined}
31
+ >
32
+ <Icon size={16} className="stats-nav-rail-item-icon" />
33
+ <span className="stats-nav-rail-item-label">{route.label}</span>
34
+ </button>
35
+ );
36
+ })}
37
+ </nav>
38
+
39
+ <div className="stats-nav-rail-footer">
40
+ <span className="stats-version-tag">OMP Stats v1.0.0</span>
41
+ </div>
42
+ </aside>
43
+ );
44
+ }
@@ -0,0 +1,39 @@
1
+ import type { TimeRange } from "../types";
2
+
3
+ export interface RangeControlProps {
4
+ value: TimeRange;
5
+ onChange: (value: TimeRange) => void;
6
+ className?: string;
7
+ }
8
+
9
+ const RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
10
+ { value: "1h", label: "1h" },
11
+ { value: "24h", label: "24h" },
12
+ { value: "7d", label: "7d" },
13
+ { value: "30d", label: "30d" },
14
+ { value: "90d", label: "90d" },
15
+ { value: "all", label: "All" },
16
+ ];
17
+
18
+ export function RangeControl({ value, onChange, className = "" }: RangeControlProps) {
19
+ return (
20
+ <div className={`stats-range-control ${className}`} role="radiogroup" aria-label="Select time range">
21
+ {RANGE_OPTIONS.map(opt => {
22
+ const isActive = opt.value === value;
23
+ return (
24
+ <button
25
+ key={opt.value}
26
+ type="button"
27
+ role="radio"
28
+ aria-checked={isActive}
29
+ data-active={isActive ? "true" : "false"}
30
+ className="stats-range-control-btn"
31
+ onClick={() => onChange(opt.value)}
32
+ >
33
+ {opt.label}
34
+ </button>
35
+ );
36
+ })}
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,75 @@
1
+ import { RefreshCw } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { sync } from "../api";
4
+
5
+ export interface SyncButtonProps {
6
+ onSyncStart?: () => void;
7
+ onSyncComplete?: (result: {
8
+ success: boolean;
9
+ data?: { processed: number; files: number; totalMessages: number };
10
+ error?: string;
11
+ }) => void;
12
+ className?: string;
13
+ }
14
+
15
+ export function SyncButton({ onSyncStart, onSyncComplete, className = "" }: SyncButtonProps) {
16
+ const [syncing, setSyncing] = useState(false);
17
+ const [status, setStatus] = useState<{ type: "success" | "error"; message: string } | null>(null);
18
+
19
+ const handleSync = async () => {
20
+ if (syncing) return;
21
+
22
+ setSyncing(true);
23
+ setStatus(null);
24
+ if (onSyncStart) {
25
+ onSyncStart();
26
+ }
27
+
28
+ try {
29
+ const data = await sync();
30
+ const result = {
31
+ processed: typeof data?.processed === "number" ? data.processed : 0,
32
+ files: typeof data?.files === "number" ? data.files : 0,
33
+ totalMessages: typeof data?.totalMessages === "number" ? data.totalMessages : 0,
34
+ };
35
+ setStatus({
36
+ type: "success",
37
+ message: `Synced: ${result.processed} new request${result.processed === 1 ? "" : "s"} found.`,
38
+ });
39
+ if (onSyncComplete) {
40
+ onSyncComplete({ success: true, data: result });
41
+ }
42
+ } catch (err) {
43
+ const errorMessage = err instanceof Error ? err.message : String(err);
44
+ setStatus({
45
+ type: "error",
46
+ message: `Sync failed: ${errorMessage}`,
47
+ });
48
+ if (onSyncComplete) {
49
+ onSyncComplete({ success: false, error: errorMessage });
50
+ }
51
+ } finally {
52
+ setSyncing(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <div className={`stats-sync-container ${className}`}>
58
+ {status && (
59
+ <span className="stats-sync-status-msg" data-type={status.type}>
60
+ {status.message}
61
+ </span>
62
+ )}
63
+ <button
64
+ type="button"
65
+ onClick={handleSync}
66
+ disabled={syncing}
67
+ className="stats-button stats-button-primary stats-sync-btn"
68
+ aria-busy={syncing}
69
+ >
70
+ <RefreshCw size={14} className={`stats-sync-icon ${syncing ? "stats-spin" : ""}`} />
71
+ {syncing ? "Syncing..." : "Sync DB"}
72
+ </button>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,37 @@
1
+ import { type LucideIcon, Monitor, Moon, Sun } from "lucide-react";
2
+ import { type ThemePreference, useThemePreference } from "../useSystemTheme";
3
+
4
+ const NEXT_PREFERENCE: Record<ThemePreference, ThemePreference> = {
5
+ system: "light",
6
+ light: "dark",
7
+ dark: "system",
8
+ };
9
+
10
+ const PREFERENCE_ICON: Record<ThemePreference, LucideIcon> = {
11
+ system: Monitor,
12
+ light: Sun,
13
+ dark: Moon,
14
+ };
15
+
16
+ const PREFERENCE_LABEL: Record<ThemePreference, string> = {
17
+ system: "System theme",
18
+ light: "Light theme",
19
+ dark: "Dark theme",
20
+ };
21
+
22
+ export function ThemeToggle() {
23
+ const { preference, setPreference } = useThemePreference();
24
+ const Icon = PREFERENCE_ICON[preference];
25
+
26
+ return (
27
+ <button
28
+ type="button"
29
+ className="stats-theme-toggle"
30
+ onClick={() => setPreference(NEXT_PREFERENCE[preference])}
31
+ aria-label={`${PREFERENCE_LABEL[preference]} (click to switch)`}
32
+ title={`${PREFERENCE_LABEL[preference]} — click to switch`}
33
+ >
34
+ <Icon size={16} />
35
+ </button>
36
+ );
37
+ }
@@ -0,0 +1,73 @@
1
+ import { Menu } from "lucide-react";
2
+ import type { TimeRange } from "../types";
3
+ import { RangeControl } from "./RangeControl";
4
+ import type { DashboardSection } from "./routes";
5
+ import { routes } from "./routes";
6
+ import { SyncButton } from "./SyncButton";
7
+ import { ThemeToggle } from "./ThemeToggle";
8
+
9
+ export interface TopBarProps {
10
+ activeSection: DashboardSection;
11
+ range: TimeRange;
12
+ onRangeChange: (range: TimeRange) => void;
13
+ updatedAt: number | null;
14
+ onSyncStart?: () => void;
15
+ onSyncComplete?: (result: { success: boolean }) => void;
16
+ onMenuToggle?: () => void;
17
+ className?: string;
18
+ }
19
+
20
+ export function TopBar({
21
+ activeSection,
22
+ range,
23
+ onRangeChange,
24
+ updatedAt,
25
+ onSyncStart,
26
+ onSyncComplete,
27
+ onMenuToggle,
28
+ className = "",
29
+ }: TopBarProps) {
30
+ const currentRoute = routes.find(r => r.id === activeSection);
31
+ const title = currentRoute?.label || "Observability";
32
+
33
+ const formatLastUpdated = (time: number | null) => {
34
+ if (!time) return "Not updated";
35
+ const date = new Date(time);
36
+ return `Updated ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
37
+ };
38
+
39
+ return (
40
+ <header className={`stats-top-bar ${className}`}>
41
+ <div className="stats-top-bar-left">
42
+ {onMenuToggle && (
43
+ <button
44
+ type="button"
45
+ onClick={onMenuToggle}
46
+ className="stats-mobile-menu-btn"
47
+ aria-label="Open navigation menu"
48
+ >
49
+ <Menu size={20} />
50
+ </button>
51
+ )}
52
+ <h1 className="stats-page-title">{title}</h1>
53
+ </div>
54
+
55
+ <div className="stats-top-bar-right">
56
+ <div className="stats-top-bar-meta">
57
+ <span
58
+ className="stats-last-updated"
59
+ title={updatedAt ? new Date(updatedAt).toLocaleString() : undefined}
60
+ >
61
+ {formatLastUpdated(updatedAt)}
62
+ </span>
63
+ </div>
64
+
65
+ <RangeControl value={range} onChange={onRangeChange} />
66
+
67
+ <ThemeToggle />
68
+
69
+ <SyncButton onSyncStart={onSyncStart} onSyncComplete={onSyncComplete} />
70
+ </div>
71
+ </header>
72
+ );
73
+ }
@@ -0,0 +1,69 @@
1
+ import { Activity, AlertCircle, Coins, Cpu, Folder, LayoutDashboard, Smile, TrendingUp, Wrench } from "lucide-react";
2
+ import type React from "react";
3
+
4
+ export type DashboardSection =
5
+ | "overview"
6
+ | "requests"
7
+ | "errors"
8
+ | "models"
9
+ | "tools"
10
+ | "costs"
11
+ | "behavior"
12
+ | "projects"
13
+ | "gain";
14
+
15
+ export interface DashboardRoute {
16
+ id: DashboardSection;
17
+ label: string;
18
+ shortLabel?: string;
19
+ icon: React.ComponentType<{ size?: number; className?: string }>;
20
+ }
21
+
22
+ export const routes: DashboardRoute[] = [
23
+ {
24
+ id: "overview",
25
+ label: "Overview",
26
+ icon: LayoutDashboard,
27
+ },
28
+ {
29
+ id: "requests",
30
+ label: "Requests",
31
+ icon: Activity,
32
+ },
33
+ {
34
+ id: "errors",
35
+ label: "Errors",
36
+ icon: AlertCircle,
37
+ },
38
+ {
39
+ id: "models",
40
+ label: "Models",
41
+ icon: Cpu,
42
+ },
43
+ {
44
+ id: "tools",
45
+ label: "Tools",
46
+ icon: Wrench,
47
+ },
48
+ {
49
+ id: "costs",
50
+ label: "Costs",
51
+ icon: Coins,
52
+ },
53
+ {
54
+ id: "behavior",
55
+ label: "Behavior",
56
+ shortLabel: "Behavior",
57
+ icon: Smile,
58
+ },
59
+ {
60
+ id: "projects",
61
+ label: "Projects",
62
+ icon: Folder,
63
+ },
64
+ {
65
+ id: "gain",
66
+ label: "Gain",
67
+ icon: TrendingUp,
68
+ },
69
+ ];
@@ -0,0 +1,68 @@
1
+ import { useMemo } from "react";
2
+ import { formatCompact, formatInteger, formatPercent } from "../data/formatters";
3
+ import { buildAgentTokenShare } from "../data/view-models";
4
+ import type { AgentType, AgentTypeStats } from "../types";
5
+
6
+ /**
7
+ * Per-agent-type display chrome. Colors follow the OMP brand palette
8
+ * (pink -> violet -> cyan) used by the dashboard charts so the bar reads on
9
+ * both themes without per-theme overrides.
10
+ */
11
+ const AGENT_META: Record<AgentType, { label: string; color: string }> = {
12
+ main: { label: "Main agent", color: "#ed4abf" },
13
+ subagent: { label: "Subagents", color: "#9b4dff" },
14
+ advisor: { label: "Advisor", color: "#5ad8e6" },
15
+ };
16
+
17
+ export interface AgentTokenShareProps {
18
+ stats: AgentTypeStats[];
19
+ }
20
+
21
+ export function AgentTokenShare({ stats }: AgentTokenShareProps) {
22
+ const view = useMemo(() => buildAgentTokenShare(stats), [stats]);
23
+
24
+ if (view.totalTokens === 0) {
25
+ return <div className="py-8 text-center stats-text-muted text-sm">No token usage in this range</div>;
26
+ }
27
+
28
+ return (
29
+ <div className="space-y-4">
30
+ <div className="flex h-3 w-full overflow-hidden rounded-full" style={{ background: "var(--surface-2)" }}>
31
+ {view.segments.map(
32
+ seg =>
33
+ seg.share > 0 && (
34
+ <div
35
+ key={seg.agentType}
36
+ className="h-full"
37
+ style={{ width: `${seg.share * 100}%`, background: AGENT_META[seg.agentType].color }}
38
+ title={`${AGENT_META[seg.agentType].label}: ${formatPercent(seg.share)}`}
39
+ />
40
+ ),
41
+ )}
42
+ </div>
43
+
44
+ <div className="space-y-2">
45
+ {view.segments.map(seg => (
46
+ <div key={seg.agentType} className="flex items-center justify-between gap-3 text-sm">
47
+ <div className="flex items-center gap-2 min-w-0">
48
+ <span
49
+ className="w-2.5 h-2.5 rounded-full flex-shrink-0"
50
+ style={{ background: AGENT_META[seg.agentType].color }}
51
+ />
52
+ <span className="stats-text-primary truncate">{AGENT_META[seg.agentType].label}</span>
53
+ <span className="stats-text-muted stats-text-xs whitespace-nowrap">
54
+ {formatInteger(seg.requests)} req
55
+ </span>
56
+ </div>
57
+ <div className="flex items-center gap-3 whitespace-nowrap">
58
+ <span className="stats-text-secondary">{formatCompact(seg.tokens)} tok</span>
59
+ <span className="stats-font-semibold stats-text-primary tabular-nums">
60
+ {formatPercent(seg.share)}
61
+ </span>
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ );
68
+ }