@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +16 -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 +123 -40
  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,123 +1,311 @@
1
- import { useMemo } from "react";
1
+ import { useMemo, useState } from "react";
2
2
  import { getRecentRequests } from "../api";
3
- import { formatDurationMs, formatInteger, formatMessageCost, formatRelativeTime } from "../data/formatters";
4
- import { useResource } from "../data/useResource";
3
+ import {
4
+ formatCompact,
5
+ formatDurationMs,
6
+ formatEstimatedCost,
7
+ formatFolder,
8
+ formatInteger,
9
+ formatMessageCost,
10
+ formatPercent,
11
+ formatRelativeTime,
12
+ formatTimestamp,
13
+ } from "../data/formatters";
14
+ import { useQuery } from "../data/query";
15
+ import { rangeMeta } from "../data/range";
16
+ import { type RequestStatus, requestStatus, summarizeRequests } from "../data/view-models";
17
+ import { REQUEST_STATUS } from "../ui/RequestDrawer";
5
18
  import type { MessageStats, TimeRange } from "../types";
6
- import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
19
+ import {
20
+ Card,
21
+ type Column,
22
+ Dot,
23
+ EmptyState,
24
+ LabelCell,
25
+ PageHeader,
26
+ QueryView,
27
+ SearchInput,
28
+ Segmented,
29
+ Skeleton,
30
+ Stat,
31
+ StatGrid,
32
+ Table,
33
+ TableSkeleton,
34
+ } from "../ui";
7
35
 
8
36
  export interface RequestsRouteProps {
9
37
  active: boolean;
10
38
  range: TimeRange;
11
- refreshTrigger: number;
12
39
  onRequestClick: (id: number) => void;
13
40
  }
14
41
 
15
- export function RequestsRoute({ active, refreshTrigger, onRequestClick }: RequestsRouteProps) {
16
- const {
17
- data: recentRequests,
18
- error,
19
- loading,
20
- } = useResource(["recent-requests-dense", refreshTrigger], signal => getRecentRequests(50, signal), {
21
- pollMs: 30000,
22
- enabled: active,
23
- });
42
+ /** How many of the newest requests to load; "Load more" steps through these. */
43
+ const LOAD_STEPS = [500, 2_000, 10_000] as const;
24
44
 
25
- const columns = useMemo(
26
- () => [
27
- {
28
- key: "model",
29
- header: "Model",
30
- render: (item: MessageStats) => (
31
- <div>
32
- <div className="stats-font-medium stats-text-primary">{item.model}</div>
33
- <div className="stats-text-xs stats-text-muted">{item.provider}</div>
34
- </div>
35
- ),
36
- },
37
- {
38
- key: "timestamp",
39
- header: "Time",
40
- render: (item: MessageStats) => formatRelativeTime(item.timestamp),
41
- },
42
- {
43
- key: "tokens",
44
- header: "Tokens",
45
- numeric: true,
46
- render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
47
- },
48
- {
49
- key: "cost",
50
- header: "API-equivalent estimate",
51
- numeric: true,
52
- render: (item: MessageStats) => formatMessageCost(item, 4),
53
- },
54
- {
55
- key: "duration",
56
- header: "Duration",
57
- numeric: true,
58
- render: (item: MessageStats) => formatDurationMs(item.duration),
59
- },
60
- {
61
- key: "status",
62
- header: "Status",
63
- className: "stats-text-center",
64
- render: (item: MessageStats) => (
65
- <StatusPill variant={item.errorMessage ? "danger" : "success"}>
66
- {item.errorMessage ? "Failed" : "Success"}
67
- </StatusPill>
68
- ),
69
- },
70
- ],
71
- [],
72
- );
45
+ type StatusFilter = "all" | RequestStatus;
73
46
 
74
- const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
75
- <div className="stats-mobile-card" onClick={onClick}>
76
- <div className="stats-mobile-card-header">
77
- <div>
78
- <div className="stats-font-semibold stats-text-primary">{item.model}</div>
79
- <div className="stats-text-xs stats-text-muted">{item.provider}</div>
80
- </div>
81
- <StatusPill variant={item.errorMessage ? "danger" : "success"}>
82
- {item.errorMessage ? "Failed" : "Success"}
83
- </StatusPill>
84
- </div>
85
- <div className="stats-mobile-card-grid">
86
- <div>
87
- <div className="stats-mobile-card-label">Time</div>
88
- <div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
89
- </div>
90
- <div>
91
- <div className="stats-mobile-card-label">API-equivalent estimate</div>
92
- <div className="stats-mobile-card-value">{formatMessageCost(item, 4)}</div>
93
- </div>
94
- <div>
95
- <div className="stats-mobile-card-label">Tokens</div>
96
- <div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
97
- </div>
98
- <div>
99
- <div className="stats-mobile-card-label">Duration</div>
100
- <div className="stats-mobile-card-value">{formatDurationMs(item.duration)}</div>
101
- </div>
102
- </div>
103
- {item.errorMessage && <div className="stats-mobile-card-error truncate mt-2">{item.errorMessage}</div>}
104
- </div>
105
- );
47
+ export function RequestsRoute({ active, range, onRequestClick }: RequestsRouteProps) {
48
+ const [step, setStep] = useState(0);
49
+ const limit = LOAD_STEPS[step];
50
+ const log = useQuery(["requests-log", limit], () => getRecentRequests(limit), { enabled: active });
51
+ const [search, setSearch] = useState("");
52
+ const [status, setStatus] = useState<StatusFilter>("all");
53
+ const meta = rangeMeta(range);
54
+
55
+ const view = useMemo(() => {
56
+ const rows = log.data ?? [];
57
+ const cutoff = meta.spanMs === null ? null : Date.now() - meta.spanMs;
58
+ const inRange = cutoff === null ? rows : rows.filter(row => row.timestamp >= cutoff);
59
+ // The loaded window covers the whole range when the server ran out of rows
60
+ // or the oldest loaded row is already older than the range start. Stale
61
+ // rows belong to a smaller limit that is being replaced.
62
+ const complete = !log.stale && (rows.length < limit || (cutoff !== null && inRange.length < rows.length));
63
+ const counts: Record<StatusFilter, number> = { all: inRange.length, ok: 0, aborted: 0, failed: 0 };
64
+ for (const row of inRange) counts[requestStatus(row)]++;
65
+ return { loaded: rows.length, inRange, complete, counts, summary: summarizeRequests(inRange) };
66
+ }, [log.data, log.stale, limit, meta.spanMs]);
67
+
68
+ const filtered = useMemo(() => {
69
+ const needle = search.trim().toLowerCase();
70
+ return view.inRange.filter(
71
+ row =>
72
+ (status === "all" || requestStatus(row) === status) &&
73
+ (needle === "" ||
74
+ row.model.toLowerCase().includes(needle) ||
75
+ row.provider.toLowerCase().includes(needle) ||
76
+ row.folder.toLowerCase().includes(needle)),
77
+ );
78
+ }, [view.inRange, search, status]);
79
+
80
+ const { summary, counts, complete } = view;
81
+ const nextStep = step + 1 < LOAD_STEPS.length ? LOAD_STEPS[step + 1] : null;
82
+ const loadMore =
83
+ nextStep !== null ? (
84
+ <button
85
+ type="button"
86
+ className="btn"
87
+ data-size="sm"
88
+ disabled={log.refreshing}
89
+ onClick={() => setStep(step + 1)}
90
+ >
91
+ {log.refreshing && log.stale ? "Loading…" : `Load latest ${formatInteger(nextStep)}`}
92
+ </button>
93
+ ) : undefined;
94
+
95
+ const statusOptions = (["all", "ok", "aborted", "failed"] as const).map(value => ({
96
+ value,
97
+ label: (
98
+ <>
99
+ {value === "all" ? "All" : REQUEST_STATUS[value].label}{" "}
100
+ <span className="dim num">{formatCompact(counts[value])}</span>
101
+ </>
102
+ ),
103
+ }));
106
104
 
107
105
  return (
108
- <div className="stats-route-container">
109
- <Panel title="All Recent Requests" subtitle="Up to 50 most recent requests processed by OMP">
110
- <AsyncBoundary loading={loading} error={error} data={recentRequests}>
111
- <DataTable
112
- columns={columns}
113
- data={recentRequests || []}
114
- keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
115
- onRowClick={item => item.id && onRequestClick(item.id)}
116
- renderMobileCard={renderMobileCard}
117
- emptyText="No recent requests found"
118
- />
119
- </AsyncBoundary>
120
- </Panel>
106
+ <div className="page">
107
+ <PageHeader
108
+ title="Requests"
109
+ description={`Every model call omp made in ${meta.windowLabel}, newest first. Open a row for its full payload.`}
110
+ />
111
+
112
+ <QueryView query={log} skeleton={<Skeleton height={96} style={{ borderRadius: 12 }} />}>
113
+ {() => (
114
+ <div data-stale={log.stale}>
115
+ <StatGrid min={160}>
116
+ <Stat
117
+ label="Requests"
118
+ value={formatInteger(summary.requests)}
119
+ hint={
120
+ summary.oldest === null
121
+ ? `none in ${meta.windowLabel}`
122
+ : `since ${formatRelativeTime(summary.oldest)}`
123
+ }
124
+ />
125
+ <Stat
126
+ label="Failed"
127
+ value={formatInteger(summary.failed)}
128
+ hint={`${summary.requests > 0 ? formatPercent(summary.failed / summary.requests) : "–"} · ${formatInteger(summary.aborted)} aborted`}
129
+ />
130
+ <Stat
131
+ label="Tokens"
132
+ title="Total tokens across these requests"
133
+ value={formatCompact(summary.tokens)}
134
+ />
135
+ <Stat
136
+ label="API-equivalent cost"
137
+ title="What these requests would cost at public API rates"
138
+ value={formatEstimatedCost(summary.cost, summary.unpriced)}
139
+ hint={summary.unpriced > 0 ? `${formatInteger(summary.unpriced)} unpriced` : undefined}
140
+ />
141
+ <Stat
142
+ label="Median duration"
143
+ value={formatDurationMs(summary.medianDuration)}
144
+ hint={`p95 ${formatDurationMs(summary.p95Duration)}`}
145
+ />
146
+ <Stat
147
+ label="Median TTFT"
148
+ title="Time to first token"
149
+ value={formatDurationMs(summary.medianTtft)}
150
+ />
151
+ </StatGrid>
152
+ </div>
153
+ )}
154
+ </QueryView>
155
+
156
+ <Card
157
+ index={1}
158
+ title="Request log"
159
+ description={
160
+ log.data === null
161
+ ? "Loading the latest requests…"
162
+ : complete
163
+ ? `${formatInteger(filtered.length)} of ${formatInteger(summary.requests)} requests in ${meta.windowLabel}`
164
+ : `${formatInteger(filtered.length)} of the latest ${formatInteger(summary.requests)} requests`
165
+ }
166
+ actions={
167
+ <>
168
+ <SearchInput value={search} onChange={setSearch} placeholder="Model, provider or project" />
169
+ <Segmented
170
+ size="sm"
171
+ aria-label="Status"
172
+ options={statusOptions}
173
+ value={status}
174
+ onChange={setStatus}
175
+ />
176
+ </>
177
+ }
178
+ flush
179
+ stale={log.stale}
180
+ footer={
181
+ log.data !== null && !complete ? (
182
+ <>
183
+ <span>
184
+ Showing the latest {formatInteger(view.loaded)} requests, back to{" "}
185
+ {summary.oldest === null ? "–" : formatTimestamp(summary.oldest)}. Older requests in{" "}
186
+ {meta.windowLabel} are not loaded.
187
+ </span>
188
+ {loadMore}
189
+ </>
190
+ ) : undefined
191
+ }
192
+ >
193
+ <QueryView query={log} skeleton={<TableSkeleton rows={12} />}>
194
+ {() => (
195
+ <Table
196
+ rows={filtered}
197
+ rowKey={row => row.id ?? `${row.sessionFile}:${row.entryId}`}
198
+ onRowClick={row => row.id !== undefined && onRequestClick(row.id)}
199
+ columns={REQUEST_COLUMNS}
200
+ initialSort={{ key: "time", dir: "desc" }}
201
+ limit={100}
202
+ dense
203
+ empty={
204
+ <EmptyState
205
+ title={summary.requests === 0 ? `No requests in ${meta.windowLabel}` : "No requests match"}
206
+ hint={summary.requests === 0 ? undefined : "Clear the search or status filter."}
207
+ />
208
+ }
209
+ />
210
+ )}
211
+ </QueryView>
212
+ </Card>
121
213
  </div>
122
214
  );
123
215
  }
216
+
217
+ const REQUEST_COLUMNS: readonly Column<MessageStats>[] = [
218
+ {
219
+ key: "model",
220
+ header: "Model",
221
+ sort: row => row.model,
222
+ render: row => <LabelCell primary={<span className="mono">{row.model}</span>} secondary={row.provider} />,
223
+ },
224
+ {
225
+ key: "time",
226
+ header: "When",
227
+ sort: row => row.timestamp,
228
+ render: row => (
229
+ <span className="muted" title={formatTimestamp(row.timestamp)}>
230
+ {formatRelativeTime(row.timestamp)}
231
+ </span>
232
+ ),
233
+ },
234
+ {
235
+ key: "project",
236
+ header: "Project",
237
+ sort: row => row.folder,
238
+ render: row => (
239
+ <span className="mono muted truncate" title={row.folder} style={{ display: "block", maxWidth: 180 }}>
240
+ {formatFolder(row.folder)}
241
+ </span>
242
+ ),
243
+ },
244
+ {
245
+ key: "input",
246
+ header: "Input",
247
+ title: "Uncached input tokens",
248
+ align: "right",
249
+ sort: row => row.usage.input,
250
+ render: row => <span className="num">{formatCompact(row.usage.input)}</span>,
251
+ },
252
+ {
253
+ key: "cache",
254
+ header: "Cache read",
255
+ title: "Cache read tokens (hover a cell for cache writes)",
256
+ align: "right",
257
+ sort: row => row.usage.cacheRead,
258
+ render: row => (
259
+ <span className="num" title={`Cache write: ${formatInteger(row.usage.cacheWrite)}`}>
260
+ {formatCompact(row.usage.cacheRead)}
261
+ </span>
262
+ ),
263
+ },
264
+ {
265
+ key: "output",
266
+ header: "Output",
267
+ align: "right",
268
+ sort: row => row.usage.output,
269
+ render: row => <span className="num">{formatCompact(row.usage.output)}</span>,
270
+ },
271
+ {
272
+ key: "cost",
273
+ header: "Cost",
274
+ title: "API-equivalent estimate",
275
+ align: "right",
276
+ sort: row => row.usage.cost.total,
277
+ render: row => <span className="num">{formatMessageCost(row, 4)}</span>,
278
+ },
279
+ {
280
+ key: "duration",
281
+ header: "Duration",
282
+ align: "right",
283
+ sort: row => row.duration ?? -1,
284
+ render: row => <span className="num">{formatDurationMs(row.duration)}</span>,
285
+ },
286
+ {
287
+ key: "ttft",
288
+ header: "TTFT",
289
+ title: "Time to first token",
290
+ align: "right",
291
+ sort: row => row.ttft ?? -1,
292
+ render: row => <span className="num muted">{formatDurationMs(row.ttft)}</span>,
293
+ },
294
+ {
295
+ key: "status",
296
+ header: "Status",
297
+ sort: row => requestStatus(row),
298
+ render: row => {
299
+ const status = REQUEST_STATUS[requestStatus(row)];
300
+ return (
301
+ <span title={row.errorMessage ?? undefined}>
302
+ <LabelCell
303
+ lead={<Dot tone={status.tone} />}
304
+ primary={status.label}
305
+ secondary={<span className="mono">{row.stopReason}</span>}
306
+ />
307
+ </span>
308
+ );
309
+ },
310
+ },
311
+ ];