@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,177 +1,319 @@
1
- import { useMemo } from "react";
1
+ import { useDeferredValue, useMemo, useState } from "react";
2
2
  import { getFolderStats } from "../api";
3
- import { formatDurationMs, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
4
- import { useResource } from "../data/useResource";
3
+ import { BarList } from "../charts";
4
+ import {
5
+ formatCompact,
6
+ formatDurationMs,
7
+ formatEstimatedCost,
8
+ formatFolder,
9
+ formatInteger,
10
+ formatPercent,
11
+ formatRelativeTime,
12
+ } from "../data/formatters";
13
+ import { useQuery } from "../data/query";
14
+ import { rangeMeta } from "../data/range";
5
15
  import { buildFolderRows, type FolderRowView } from "../data/view-models";
6
16
  import type { TimeRange } from "../types";
7
- import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
17
+ import {
18
+ Badge,
19
+ Card,
20
+ ChartSkeleton,
21
+ type Column,
22
+ EmptyState,
23
+ errorRateTone,
24
+ MeterCell,
25
+ PageHeader,
26
+ QueryView,
27
+ SearchInput,
28
+ Stat,
29
+ StatGrid,
30
+ Table,
31
+ TableSkeleton,
32
+ } from "../ui";
33
+ import "./projects.css";
8
34
 
9
35
  export interface ProjectsRouteProps {
10
36
  active: boolean;
11
37
  range: TimeRange;
12
- refreshTrigger: number;
13
38
  }
14
39
 
15
- export function ProjectsRoute({ active, range, refreshTrigger }: ProjectsRouteProps) {
16
- const {
17
- data: foldersData,
18
- error,
19
- loading,
20
- } = useResource(["projects", range, refreshTrigger], signal => getFolderStats(range, signal), {
21
- pollMs: 30000,
22
- enabled: active,
23
- });
40
+ /** Rows rendered before "Show all": keeps the first paint cheap with tens of thousands of folders. */
41
+ const TABLE_LIMIT = 100;
42
+ const TOP_LIMIT = 8;
24
43
 
25
- const folderRows = useMemo(() => {
26
- if (!foldersData) return [];
27
- return buildFolderRows(foldersData);
28
- }, [foldersData]);
44
+ export function ProjectsRoute({ active, range }: ProjectsRouteProps) {
45
+ const folders = useQuery(["projects", range], () => getFolderStats(range), { enabled: active });
46
+ const [search, setSearch] = useState("");
47
+ // Benchmarks and scratch sessions can leave tens of thousands of one-off temp folders; hide them by default.
48
+ const [hideTemporary, setHideTemporary] = useState(true);
49
+ const query = useDeferredValue(search.trim().toLowerCase());
50
+ const meta = rangeMeta(range);
29
51
 
30
- const columns = useMemo(
31
- () => [
32
- {
33
- key: "folder",
34
- header: "Project/Folder",
35
- render: (item: FolderRowView) => (
36
- <div
37
- className="stats-font-medium stats-text-primary truncate max-w-[440px]"
38
- title={item.folder || "(root)"}
39
- >
40
- {item.folder || "(root)"}
41
- </div>
42
- ),
43
- },
44
- {
45
- key: "totalRequests",
46
- header: "Requests",
47
- numeric: true,
48
- render: (item: FolderRowView) => (
49
- <div className="stats-text-right">
50
- <div className="font-mono">{formatInteger(item.totalRequests)}</div>
51
- <div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
52
- <div
53
- className="stats-progress-bar-fill"
54
- data-variant="link"
55
- style={{ width: `${item.requestsPercentage}%` }}
52
+ const view = useMemo(() => buildFolderRows(folders.data ?? []), [folders.data]);
53
+ const scoped = useMemo(
54
+ () => (hideTemporary ? view.rows.filter(row => !row.temporary) : view.rows),
55
+ [view, hideTemporary],
56
+ );
57
+ const matching = useMemo(
58
+ () => (query ? scoped.filter(row => row.folder.toLowerCase().includes(query)) : scoped),
59
+ [scoped, query],
60
+ );
61
+ const top = useMemo(
62
+ () => ({
63
+ cost: [...scoped].sort((a, b) => b.totalCost - a.totalCost).slice(0, TOP_LIMIT),
64
+ requests: [...scoped].sort((a, b) => b.totalRequests - a.totalRequests).slice(0, TOP_LIMIT),
65
+ }),
66
+ [scoped],
67
+ );
68
+ const topEmpty = (
69
+ <EmptyState
70
+ title={view.rows.length === 0 ? "No project folders in this range" : "Only temporary folders in this range"}
71
+ />
72
+ );
73
+ // Meter scales follow the whole range so a bar's length does not change while filtering.
74
+ const columns = useMemo(() => folderColumns(view.maxRequests, view.maxCost), [view]);
75
+
76
+ return (
77
+ <div className="page">
78
+ <PageHeader title="Projects" description={`Usage by session folder in ${meta.windowLabel}.`} />
79
+
80
+ <QueryView query={folders} skeleton={<ChartSkeleton height={96} />}>
81
+ {() => (
82
+ <div data-stale={folders.stale}>
83
+ <StatGrid min={180}>
84
+ <Stat
85
+ label="Folders"
86
+ value={formatInteger(view.rows.length)}
87
+ hint={view.temporaryCount > 0 ? `${formatInteger(view.temporaryCount)} temporary` : undefined}
56
88
  />
57
- </div>
58
- </div>
59
- ),
60
- },
61
- {
62
- key: "totalCost",
63
- header: "API-equivalent estimate",
64
- numeric: true,
65
- render: (item: FolderRowView) => (
66
- <div className="stats-text-right">
67
- <div className="font-mono">{formatEstimatedCost(item.totalCost, item.unpricedRequests)}</div>
68
- <div className="stats-progress-bar-track mt-1 ml-auto w-24 h-1">
69
- <div
70
- className="stats-progress-bar-fill"
71
- data-variant="success"
72
- style={{ width: `${item.costPercentage}%` }}
89
+ <Stat
90
+ label="Requests"
91
+ value={formatInteger(view.totalRequests)}
92
+ hint={`${formatInteger(view.failedRequests)} failed`}
93
+ />
94
+ <Stat
95
+ label="API-equivalent cost"
96
+ title="What this usage would cost at public API rates"
97
+ value={formatEstimatedCost(view.totalCost, view.unpricedRequests)}
98
+ hint={
99
+ view.unpricedRequests > 0 ? `${formatInteger(view.unpricedRequests)} unpriced` : undefined
100
+ }
101
+ />
102
+ <Stat
103
+ label="Conversation tokens"
104
+ title="Uncached input + cache reads + cache writes + output"
105
+ value={formatCompact(view.conversationTokens)}
106
+ />
107
+ <Stat
108
+ label="Cache rate"
109
+ title="Cache reads ÷ (uncached input + cache reads), across every folder"
110
+ value={formatPercent(view.cacheRate)}
73
111
  />
74
- </div>
112
+ </StatGrid>
75
113
  </div>
76
- ),
77
- },
78
- {
79
- key: "totalTokens",
80
- header: "Tokens",
81
- numeric: true,
82
- render: (item: FolderRowView) => (
83
- <div className="font-mono">{formatInteger(item.totalInputTokens + item.totalOutputTokens)}</div>
84
- ),
85
- },
86
- {
87
- key: "cacheRate",
88
- header: "Cache Rate",
89
- numeric: true,
90
- render: (item: FolderRowView) => <span className="font-mono">{formatPercent(item.cacheRate)}</span>,
91
- },
92
- {
93
- key: "cacheSavings",
94
- header: "Cache Savings",
95
- numeric: true,
96
- render: (item: FolderRowView) => (
97
- <span className={`${item.cacheSavings < 0 ? "stats-text-danger" : "stats-text-success"} font-medium`}>
98
- {formatPercent(item.cacheSavings)}
99
- </span>
100
- ),
101
- },
102
- {
103
- key: "errorRate",
104
- header: "Error Rate",
105
- numeric: true,
106
- render: (item: FolderRowView) => (
107
- <StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
108
- {formatPercent(item.errorRate)}
109
- </StatusPill>
110
- ),
111
- },
112
- {
113
- key: "avgDuration",
114
- header: "Avg Duration",
115
- numeric: true,
116
- render: (item: FolderRowView) => formatDurationMs(item.avgDuration),
117
- },
118
- ],
119
- [],
120
- );
114
+ )}
115
+ </QueryView>
121
116
 
122
- const renderMobileCard = (item: FolderRowView) => (
123
- <div className="stats-mobile-card">
124
- <div className="stats-mobile-card-header mb-2">
125
- <div className="stats-font-semibold stats-text-primary">{item.folder || "(root)"}</div>
126
- <StatusPill variant={item.errorRate > 0.1 ? "danger" : item.errorRate > 0 ? "warning" : "success"}>
127
- {formatPercent(item.errorRate)} Err
128
- </StatusPill>
129
- </div>
130
- <div className="stats-mobile-card-grid">
131
- <div>
132
- <div className="stats-mobile-card-label">Requests</div>
133
- <div className="stats-mobile-card-value font-mono">{formatInteger(item.totalRequests)}</div>
134
- </div>
135
- <div>
136
- <div className="stats-mobile-card-label">API-equivalent estimate</div>
137
- <div className="stats-mobile-card-value font-mono">
138
- {formatEstimatedCost(item.totalCost, item.unpricedRequests)}
139
- </div>
140
- </div>
141
- <div>
142
- <div className="stats-mobile-card-label">Cache Rate</div>
143
- <div className="stats-mobile-card-value">{formatPercent(item.cacheRate)}</div>
144
- </div>
145
- <div>
146
- <div className="stats-mobile-card-label">Cache Savings</div>
147
- <div className="stats-mobile-card-value">{formatPercent(item.cacheSavings)}</div>
148
- </div>
149
- <div>
150
- <div className="stats-mobile-card-label">Duration</div>
151
- <div className="stats-mobile-card-value">{formatDurationMs(item.avgDuration)}</div>
152
- </div>
117
+ <div className="grid grid-2">
118
+ <Card index={1} title="Top by cost" description="Share of API-equivalent cost" stale={folders.stale}>
119
+ <QueryView
120
+ query={folders}
121
+ skeleton={<ChartSkeleton height={236} />}
122
+ isEmpty={() => top.cost.length === 0}
123
+ empty={topEmpty}
124
+ >
125
+ {() => (
126
+ <BarList
127
+ items={top.cost.map(row => ({
128
+ key: row.folder,
129
+ label: <span className="mono">{formatFolder(row.folder)}</span>,
130
+ value: row.totalCost,
131
+ display: `${formatEstimatedCost(row.totalCost, row.unpricedRequests)} · ${formatPercent(row.costShare)}`,
132
+ color: "var(--chart-secondary)",
133
+ }))}
134
+ onSelect={setSearch}
135
+ />
136
+ )}
137
+ </QueryView>
138
+ </Card>
139
+ <Card index={2} title="Top by requests" description="Share of all requests" stale={folders.stale}>
140
+ <QueryView
141
+ query={folders}
142
+ skeleton={<ChartSkeleton height={236} />}
143
+ isEmpty={() => top.requests.length === 0}
144
+ empty={topEmpty}
145
+ >
146
+ {() => (
147
+ <BarList
148
+ items={top.requests.map(row => ({
149
+ key: row.folder,
150
+ label: <span className="mono">{formatFolder(row.folder)}</span>,
151
+ value: row.totalRequests,
152
+ display: `${formatInteger(row.totalRequests)} · ${formatPercent(row.requestShare)}`,
153
+ }))}
154
+ onSelect={setSearch}
155
+ />
156
+ )}
157
+ </QueryView>
158
+ </Card>
153
159
  </div>
160
+
161
+ <Card
162
+ index={3}
163
+ title="Folders"
164
+ description={
165
+ folders.data
166
+ ? matching.length === view.rows.length
167
+ ? `${formatInteger(view.rows.length)} folders`
168
+ : `${formatInteger(matching.length)} of ${formatInteger(view.rows.length)} folders`
169
+ : undefined
170
+ }
171
+ actions={
172
+ <>
173
+ {view.temporaryCount > 0 && (
174
+ <label className="check" title="Session folders under /tmp or /var/folders">
175
+ <input
176
+ type="checkbox"
177
+ checked={hideTemporary}
178
+ onChange={e => setHideTemporary(e.target.checked)}
179
+ />
180
+ Hide temporary ({formatInteger(view.temporaryCount)})
181
+ </label>
182
+ )}
183
+ <SearchInput value={search} onChange={setSearch} placeholder="Filter folders…" width={240} />
184
+ </>
185
+ }
186
+ flush
187
+ stale={folders.stale}
188
+ >
189
+ <QueryView query={folders} skeleton={<TableSkeleton rows={10} />}>
190
+ {() => (
191
+ <Table
192
+ rows={matching}
193
+ rowKey={row => row.folder}
194
+ columns={columns}
195
+ initialSort={{ key: "cost", dir: "desc" }}
196
+ limit={TABLE_LIMIT}
197
+ dense
198
+ empty={
199
+ <EmptyState
200
+ title={view.rows.length === 0 ? "No project folders in this range" : "No folders match"}
201
+ hint={
202
+ view.rows.length === 0
203
+ ? "Try a longer range."
204
+ : hideTemporary && view.temporaryCount > 0
205
+ ? "Temporary folders are hidden."
206
+ : undefined
207
+ }
208
+ />
209
+ }
210
+ />
211
+ )}
212
+ </QueryView>
213
+ </Card>
154
214
  </div>
155
215
  );
216
+ }
156
217
 
157
- return (
158
- <div className="stats-route-container">
159
- <Panel title="Projects & Folders" subtitle="Aggregate proxy metrics grouped by folder path">
160
- <AsyncBoundary
161
- loading={loading}
162
- error={error}
163
- data={foldersData}
164
- emptyText="No project folders recorded for this range."
165
- >
166
- <DataTable
167
- columns={columns}
168
- data={folderRows}
169
- keyExtractor={item => item.folder}
170
- renderMobileCard={renderMobileCard}
171
- emptyText="No project folders recorded for this range."
218
+ function folderColumns(maxRequests: number, maxCost: number): Column<FolderRowView>[] {
219
+ return [
220
+ {
221
+ key: "folder",
222
+ header: "Folder",
223
+ sort: row => row.folder,
224
+ render: row => (
225
+ <span className="row projects-folder" title={row.folder || "(root)"}>
226
+ <span className="mono truncate">{formatFolder(row.folder)}</span>
227
+ {row.temporary && <Badge>temp</Badge>}
228
+ </span>
229
+ ),
230
+ },
231
+ {
232
+ key: "requests",
233
+ header: "Requests",
234
+ align: "right",
235
+ sort: row => row.totalRequests,
236
+ render: row => (
237
+ <span title={`${formatPercent(row.requestShare)} of all requests`}>
238
+ <MeterCell value={row.totalRequests} max={maxRequests} display={formatInteger(row.totalRequests)} />
239
+ </span>
240
+ ),
241
+ },
242
+ {
243
+ key: "cost",
244
+ header: "Cost",
245
+ title: "API-equivalent cost at public API rates",
246
+ align: "right",
247
+ sort: row => row.totalCost,
248
+ render: row => (
249
+ <span title={`${formatPercent(row.costShare)} of all cost`}>
250
+ <MeterCell
251
+ value={row.totalCost}
252
+ max={maxCost}
253
+ display={formatEstimatedCost(row.totalCost, row.unpricedRequests)}
254
+ color="var(--chart-secondary)"
172
255
  />
173
- </AsyncBoundary>
174
- </Panel>
175
- </div>
176
- );
256
+ </span>
257
+ ),
258
+ },
259
+ {
260
+ key: "tokens",
261
+ header: "Tokens",
262
+ title: "Uncached input + cache reads + cache writes + output",
263
+ align: "right",
264
+ sort: row => row.conversationTokens,
265
+ render: row => (
266
+ <span className="num" title={formatInteger(row.conversationTokens)}>
267
+ {formatCompact(row.conversationTokens)}
268
+ </span>
269
+ ),
270
+ },
271
+ {
272
+ key: "cacheRate",
273
+ header: "Cache rate",
274
+ title: "Cache reads ÷ (uncached input + cache reads)",
275
+ align: "right",
276
+ sort: row => row.cacheRate,
277
+ render: row => <span className="num">{formatPercent(row.cacheRate)}</span>,
278
+ },
279
+ {
280
+ key: "cacheSavings",
281
+ header: "Cache savings",
282
+ title: "Prompt-input cost saved versus billing the same tokens uncached",
283
+ align: "right",
284
+ sort: row => row.cacheSavings,
285
+ render: row => (
286
+ <span className={`num ${row.cacheSavings < 0 ? "tone-bad" : "muted"}`}>
287
+ {formatPercent(row.cacheSavings)}
288
+ </span>
289
+ ),
290
+ },
291
+ {
292
+ key: "errorRate",
293
+ header: "Errors",
294
+ align: "right",
295
+ sort: row => row.errorRate,
296
+ render: row => (
297
+ <span title={`${formatInteger(row.failedRequests)} failed`}>
298
+ <Badge tone={row.failedRequests > 0 ? errorRateTone(row.errorRate) : "neutral"} mono>
299
+ {formatPercent(row.errorRate)}
300
+ </Badge>
301
+ </span>
302
+ ),
303
+ },
304
+ {
305
+ key: "duration",
306
+ header: "Avg duration",
307
+ align: "right",
308
+ sort: row => row.avgDuration ?? -1,
309
+ render: row => <span className="num">{formatDurationMs(row.avgDuration)}</span>,
310
+ },
311
+ {
312
+ key: "last",
313
+ header: "Last active",
314
+ align: "right",
315
+ sort: row => row.lastTimestamp,
316
+ render: row => <span className="dim">{formatRelativeTime(row.lastTimestamp)}</span>,
317
+ },
318
+ ];
177
319
  }