@oh-my-pi/omp-stats 18.3.5 → 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
@@ -5,185 +5,204 @@
5
5
 
6
6
  import { useMemo, useState } from "react";
7
7
  import { getSessions } from "../api";
8
- import { formatCompact, formatDurationMs, formatEstimatedCost, formatRelativeTime } from "../data/formatters";
9
- import { useResource } from "../data/useResource";
8
+ import {
9
+ formatCompact,
10
+ formatElapsed,
11
+ formatEstimatedCost,
12
+ formatInteger,
13
+ formatRelativeTime,
14
+ } from "../data/formatters";
15
+ import { useQuery } from "../data/query";
10
16
  import { TraceView } from "../traces/TraceView";
11
17
  import type { SessionSummary } from "../types";
12
- import { AsyncBoundary, DataTable, Panel } from "../ui";
18
+ import {
19
+ Card,
20
+ type Column,
21
+ EmptyState,
22
+ LabelCell,
23
+ MeterCell,
24
+ PageHeader,
25
+ QueryView,
26
+ SearchInput,
27
+ Table,
28
+ TableSkeleton,
29
+ } from "../ui";
30
+ import "../traces/traces.css";
13
31
 
14
32
  export interface TracesRouteProps {
15
33
  active: boolean;
16
34
  session: string | null;
17
35
  onOpenSession: (file: string | null) => void;
18
- refreshTrigger: number;
19
36
  }
20
37
 
21
- function ModelChips({ models }: { models: string[] }) {
22
- const shown = models.slice(0, 3);
38
+ const SESSION_LIMIT = 200;
39
+
40
+ function ModelList({ models }: { models: string[] }) {
41
+ if (models.length === 0) return <span className="dim">-</span>;
23
42
  return (
24
- <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
25
- {shown.map(model => (
26
- <span
27
- key={model}
28
- className="stats-text-muted truncate"
29
- style={{
30
- fontSize: 10,
31
- border: "1px solid var(--border)",
32
- borderRadius: 999,
33
- padding: "1px 6px",
34
- maxWidth: 140,
35
- }}
36
- >
37
- {model}
38
- </span>
39
- ))}
40
- {models.length > 3 && (
41
- <span className="stats-text-muted" style={{ fontSize: 10 }}>
42
- +{models.length - 3}
43
- </span>
44
- )}
45
- </div>
43
+ <span className="traces-models" title={models.join("\n")}>
44
+ <span className="mono truncate">{models[0]}</span>
45
+ {models.length > 1 && <span className="badge">+{models.length - 1}</span>}
46
+ </span>
46
47
  );
47
48
  }
48
49
 
49
- export function TracesRoute({ active, session, onOpenSession, refreshTrigger }: TracesRouteProps) {
50
+ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps) {
50
51
  const [filter, setFilter] = useState("");
51
-
52
- const {
53
- data: sessions,
54
- error,
55
- loading,
56
- } = useResource(["sessions", refreshTrigger], signal => getSessions(200, undefined, signal), {
52
+ const sessions = useQuery(["sessions", SESSION_LIMIT], () => getSessions(SESSION_LIMIT), {
57
53
  pollMs: 30000,
58
54
  enabled: active && session === null,
59
55
  });
60
56
 
61
57
  const filtered = useMemo(() => {
62
- if (!sessions) return [];
58
+ const rows = sessions.data ?? [];
63
59
  const needle = filter.trim().toLowerCase();
64
- if (!needle) return sessions;
65
- return sessions.filter(
60
+ if (!needle) return rows;
61
+ return rows.filter(
66
62
  row =>
67
63
  (row.title ?? "").toLowerCase().includes(needle) ||
68
64
  row.folder.toLowerCase().includes(needle) ||
69
65
  row.models.some(model => model.toLowerCase().includes(needle)),
70
66
  );
71
- }, [sessions, filter]);
67
+ }, [sessions.data, filter]);
72
68
 
73
- const columns = useMemo(
69
+ const maxCost = useMemo(() => Math.max(0, ...filtered.map(row => row.costTotal)), [filtered]);
70
+
71
+ const columns = useMemo<Column<SessionSummary>[]>(
74
72
  () => [
75
73
  {
76
74
  key: "title",
77
- header: "Title",
78
- render: (item: SessionSummary) => (
79
- <div className="stats-font-medium stats-text-primary truncate" style={{ maxWidth: 280 }}>
80
- {item.title ?? item.file.split("/").pop()}
81
- </div>
82
- ),
83
- },
84
- {
85
- key: "folder",
86
- header: "Project",
87
- render: (item: SessionSummary) => (
88
- <span className="stats-text-muted truncate" style={{ maxWidth: 160, display: "inline-block" }}>
89
- {item.folder.split("/").slice(-2).join("/")}
90
- </span>
75
+ header: "Session",
76
+ width: "32%",
77
+ render: row => (
78
+ <LabelCell
79
+ primary={row.title ?? row.file.split("/").pop()}
80
+ secondary={<span className="mono">{row.folder.split("/").slice(-2).join("/")}</span>}
81
+ />
91
82
  ),
83
+ sort: row => (row.title ?? row.file).toLowerCase(),
92
84
  },
85
+ { key: "models", header: "Models", render: row => <ModelList models={row.models} /> },
93
86
  {
94
87
  key: "started",
95
88
  header: "Started",
96
- render: (item: SessionSummary) => formatRelativeTime(item.startedAt),
89
+ align: "right",
90
+ render: row => (
91
+ <span className="muted" title={new Date(row.startedAt).toLocaleString()}>
92
+ {formatRelativeTime(row.startedAt)}
93
+ </span>
94
+ ),
95
+ sort: row => row.startedAt,
97
96
  },
98
97
  {
99
98
  key: "duration",
100
99
  header: "Duration",
101
- numeric: true,
102
- render: (item: SessionSummary) => formatDurationMs(item.endedAt - item.startedAt),
100
+ align: "right",
101
+ render: row => <span className="num">{formatElapsed(row.endedAt - row.startedAt)}</span>,
102
+ sort: row => row.endedAt - row.startedAt,
103
+ },
104
+ {
105
+ key: "requests",
106
+ header: "Requests",
107
+ align: "right",
108
+ render: row => <span className="num">{formatInteger(row.requests)}</span>,
109
+ sort: row => row.requests,
110
+ },
111
+ {
112
+ key: "toolCalls",
113
+ header: "Tools",
114
+ align: "right",
115
+ render: row => <span className="num">{formatInteger(row.toolCalls)}</span>,
116
+ sort: row => row.toolCalls,
117
+ },
118
+ {
119
+ key: "subagents",
120
+ header: "Agents",
121
+ align: "right",
122
+ render: row =>
123
+ row.subagents > 0 ? (
124
+ <span className="num">{formatInteger(row.subagents)}</span>
125
+ ) : (
126
+ <span className="num dim">0</span>
127
+ ),
128
+ sort: row => row.subagents,
103
129
  },
104
- { key: "requests", header: "Requests", numeric: true, render: (item: SessionSummary) => item.requests },
105
- { key: "toolCalls", header: "Tools", numeric: true, render: (item: SessionSummary) => item.toolCalls },
106
- { key: "subagents", header: "Agents", numeric: true, render: (item: SessionSummary) => item.subagents },
107
130
  {
108
131
  key: "tokens",
109
132
  header: "Tokens",
110
- numeric: true,
111
- render: (item: SessionSummary) => formatCompact(item.totalTokens),
133
+ align: "right",
134
+ render: row => <span className="num">{formatCompact(row.totalTokens)}</span>,
135
+ sort: row => row.totalTokens,
112
136
  },
113
137
  {
114
138
  key: "cost",
115
139
  header: "Cost",
116
- numeric: true,
117
- render: (item: SessionSummary) => formatEstimatedCost(item.costTotal, item.unpricedRequests),
140
+ align: "right",
141
+ width: 150,
142
+ render: row => (
143
+ <MeterCell
144
+ value={row.costTotal}
145
+ max={maxCost}
146
+ display={formatEstimatedCost(row.costTotal, row.unpricedRequests)}
147
+ />
148
+ ),
149
+ sort: row => row.costTotal,
118
150
  },
119
- { key: "models", header: "Models", render: (item: SessionSummary) => <ModelChips models={item.models} /> },
120
151
  ],
121
- [],
122
- );
123
-
124
- const renderMobileCard = (item: SessionSummary, onClick?: () => void) => (
125
- <div className="stats-mobile-card" onClick={onClick}>
126
- <div className="stats-mobile-card-header">
127
- <div className="stats-font-semibold stats-text-primary truncate">
128
- {item.title ?? item.file.split("/").pop()}
129
- </div>
130
- </div>
131
- <div className="stats-mobile-card-grid">
132
- <div>
133
- <div className="stats-mobile-card-label">Started</div>
134
- <div className="stats-mobile-card-value">{formatRelativeTime(item.startedAt)}</div>
135
- </div>
136
- <div>
137
- <div className="stats-mobile-card-label">Duration</div>
138
- <div className="stats-mobile-card-value">{formatDurationMs(item.endedAt - item.startedAt)}</div>
139
- </div>
140
- <div>
141
- <div className="stats-mobile-card-label">Requests</div>
142
- <div className="stats-mobile-card-value">{item.requests}</div>
143
- </div>
144
- <div>
145
- <div className="stats-mobile-card-label">Cost</div>
146
- <div className="stats-mobile-card-value">
147
- {formatEstimatedCost(item.costTotal, item.unpricedRequests)}
148
- </div>
149
- </div>
150
- </div>
151
- </div>
152
+ [maxCost],
152
153
  );
153
154
 
154
155
  if (session !== null) {
155
156
  return <TraceView file={session} active={active} onBack={() => onOpenSession(null)} />;
156
157
  }
157
158
 
159
+ const total = sessions.data?.length ?? 0;
158
160
  return (
159
- <div className="stats-route-container">
160
- <Panel
161
+ <div className="stack traces-page">
162
+ <PageHeader
163
+ title="Traces"
164
+ description="Recent sessions with subagent activity folded in. Open one to inspect its timeline."
165
+ />
166
+ <Card
161
167
  title="Sessions"
162
- subtitle="Recent sessions with subagent activity folded in — click one to open its trace"
168
+ description={
169
+ sessions.data
170
+ ? filter.trim()
171
+ ? `${formatInteger(filtered.length)} of ${formatInteger(total)} most recent`
172
+ : `${formatInteger(total)} most recent`
173
+ : undefined
174
+ }
163
175
  actions={
164
- <input
165
- type="search"
176
+ <SearchInput
166
177
  value={filter}
167
- onChange={event => setFilter(event.target.value)}
178
+ onChange={setFilter}
168
179
  placeholder="Filter by title, project, model…"
169
- aria-label="Filter sessions"
170
- spellCheck={false}
171
- className="stats-trace-input"
172
- style={{ width: 220 }}
180
+ width={260}
173
181
  />
174
182
  }
183
+ flush
184
+ index={0}
185
+ stale={sessions.stale}
175
186
  >
176
- <AsyncBoundary loading={loading} error={error} data={sessions}>
177
- <DataTable
178
- columns={columns}
179
- data={filtered}
180
- keyExtractor={item => item.file}
181
- onRowClick={item => onOpenSession(item.file)}
182
- renderMobileCard={renderMobileCard}
183
- emptyText="No sessions found — run a Sync to index recent activity"
184
- />
185
- </AsyncBoundary>
186
- </Panel>
187
+ <QueryView
188
+ query={sessions}
189
+ skeleton={<TableSkeleton rows={10} />}
190
+ isEmpty={rows => rows.length === 0}
191
+ empty={<EmptyState title="No sessions found" hint="Run a sync to index recent activity." />}
192
+ >
193
+ {() => (
194
+ <Table
195
+ columns={columns}
196
+ rows={filtered}
197
+ rowKey={row => row.file}
198
+ onRowClick={row => onOpenSession(row.file)}
199
+ initialSort={{ key: "started", dir: "desc" }}
200
+ limit={50}
201
+ empty={<EmptyState title="No matching sessions" hint="Try a different filter." />}
202
+ />
203
+ )}
204
+ </QueryView>
205
+ </Card>
187
206
  </div>
188
207
  );
189
208
  }
@@ -0,0 +1,24 @@
1
+ /* Costs page: component breakdown list. */
2
+
3
+ .costs-components {
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .costs-component-row {
9
+ display: grid;
10
+ grid-template-columns: minmax(0, 1fr) auto 56px;
11
+ align-items: center;
12
+ gap: 12px;
13
+ padding: 7px 0;
14
+ border-bottom: 1px solid var(--line-1);
15
+ }
16
+
17
+ .costs-component-share {
18
+ text-align: right;
19
+ }
20
+
21
+ .costs-component-total {
22
+ border-bottom: 0;
23
+ font-weight: 500;
24
+ }
@@ -0,0 +1,60 @@
1
+ /* Errors page. */
2
+ .errors-signature {
3
+ display: -webkit-box;
4
+ -webkit-line-clamp: 2;
5
+ -webkit-box-orient: vertical;
6
+ overflow: hidden;
7
+ font-family: var(--font-mono);
8
+ font-size: 12px;
9
+ line-height: 1.5;
10
+ color: var(--ink-1);
11
+ overflow-wrap: anywhere;
12
+ }
13
+
14
+ .errors-message {
15
+ display: block;
16
+ max-width: 520px;
17
+ overflow: hidden;
18
+ text-overflow: ellipsis;
19
+ white-space: nowrap;
20
+ font-family: var(--font-mono);
21
+ font-size: 12px;
22
+ color: var(--bad);
23
+ }
24
+
25
+ .errors-filter {
26
+ max-width: 260px;
27
+ }
28
+
29
+ .errors-filter .truncate {
30
+ min-width: 0;
31
+ }
32
+
33
+ .errors-detail {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 10px;
37
+ padding: 4px 16px 14px;
38
+ background: var(--hover);
39
+ border-bottom: 1px solid var(--line-1);
40
+ }
41
+
42
+ .errors-detail-message {
43
+ max-height: 200px;
44
+ color: var(--ink-1);
45
+ }
46
+
47
+ .errors-detail-meta {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: 8px;
52
+ flex-wrap: wrap;
53
+ font-size: 12.5px;
54
+ }
55
+
56
+ .errors-detail-models {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ gap: 6px;
60
+ }
@@ -0,0 +1,123 @@
1
+ .frustration-judge {
2
+ gap: 10px;
3
+ font-size: 13px;
4
+ }
5
+
6
+ .frustration-spacer {
7
+ flex: 1;
8
+ }
9
+
10
+ .frustration-progress {
11
+ gap: 8px;
12
+ }
13
+
14
+ .frustration-progress-head {
15
+ gap: 10px;
16
+ }
17
+
18
+ .frustration-progress-track {
19
+ height: 6px;
20
+ border-radius: 3px;
21
+ }
22
+
23
+ .frustration-progress-fill {
24
+ border-radius: 3px;
25
+ background: var(--accent-gradient);
26
+ }
27
+
28
+ .frustration-progress-meta {
29
+ flex-wrap: wrap;
30
+ gap: 4px 18px;
31
+ }
32
+
33
+ /* Cost quote headline in the confirm dialog. */
34
+ .frustration-quote {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 2px;
38
+ padding: 12px 14px;
39
+ border: 1px solid var(--line-2);
40
+ border-radius: var(--r-control);
41
+ background: var(--hover);
42
+ }
43
+
44
+ .frustration-quote-value {
45
+ font-size: 22px;
46
+ font-weight: 600;
47
+ color: var(--ink-1);
48
+ }
49
+
50
+ .frustration-chart-body {
51
+ gap: 14px;
52
+ }
53
+
54
+ /* Many model classes: scroll sideways rather than wrap (the thumb tracks one row). */
55
+ .frustration-classes {
56
+ overflow-x: auto;
57
+ scrollbar-width: none;
58
+ }
59
+
60
+ .frustration-filters {
61
+ flex-wrap: wrap;
62
+ align-items: flex-start;
63
+ gap: 8px 16px;
64
+ }
65
+
66
+ .frustration-filters .legend {
67
+ flex: 1 1 320px;
68
+ }
69
+
70
+ .frustration-key {
71
+ padding-top: 10px;
72
+ border-top: 1px solid var(--line-1);
73
+ color: var(--ink-3);
74
+ }
75
+
76
+ .frustration-key .frustration-key-wide {
77
+ max-width: none;
78
+ }
79
+
80
+ .frustration-key-swatch[data-variant="assistant"] {
81
+ background: var(--ink-2);
82
+ }
83
+
84
+ .frustration-key-swatch[data-variant="angry"] {
85
+ background: color-mix(in srgb, var(--ink-2) 55%, black);
86
+ }
87
+
88
+ .frustration-key-swatch[data-variant="other"] {
89
+ background: color-mix(in srgb, var(--ink-2) 32%, transparent);
90
+ }
91
+
92
+ .frustration-key-swatch[data-variant="hatch"] {
93
+ background: repeating-linear-gradient(
94
+ 45deg,
95
+ var(--ink-2) 0 1.6px,
96
+ color-mix(in srgb, var(--ink-2) 14%, transparent) 1.6px 3.5px
97
+ );
98
+ }
99
+
100
+ .frustration-key-line {
101
+ width: 12px;
102
+ height: 2px;
103
+ border-radius: 1px;
104
+ background: var(--ink-1);
105
+ }
106
+
107
+ .frustration-chart-empty {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ height: 340px;
112
+ }
113
+
114
+ .frustration-tooltip-extra {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 2px;
118
+ }
119
+
120
+ .frustration-judged {
121
+ justify-content: flex-end;
122
+ gap: 6px;
123
+ }
@@ -1,6 +1,6 @@
1
- export * from "./BehaviorRoute";
2
1
  export * from "./CostsRoute";
3
2
  export * from "./ErrorsRoute";
3
+ export * from "./FrustrationRoute";
4
4
  export * from "./GainRoute";
5
5
  export * from "./ModelsRoute";
6
6
  export * from "./OverviewRoute";
@@ -0,0 +1,43 @@
1
+ /* Models page: expandable model rows. */
2
+
3
+ .models-chevron {
4
+ display: inline-flex;
5
+ color: var(--ink-3);
6
+ transition: transform var(--dur-2) var(--ease);
7
+ }
8
+
9
+ .models-chevron[data-open="true"] {
10
+ transform: rotate(90deg);
11
+ color: var(--ink-1);
12
+ }
13
+
14
+ .models-detail {
15
+ display: grid;
16
+ grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
17
+ gap: 24px;
18
+ padding: 16px 16px 18px 44px;
19
+ background: var(--panel);
20
+ border-bottom: 1px solid var(--line-1);
21
+ animation: rise var(--dur-3) var(--ease-out) both;
22
+ }
23
+
24
+ .models-detail-facts {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 16px;
28
+ min-width: 0;
29
+ }
30
+
31
+ .models-detail-chart {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 10px;
35
+ min-width: 0;
36
+ }
37
+
38
+ @media (max-width: 1100px) {
39
+ .models-detail {
40
+ grid-template-columns: minmax(0, 1fr);
41
+ padding-left: 16px;
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ /* Long temp-dir names must not stretch the table. */
2
+ .projects-folder {
3
+ gap: 8px;
4
+ max-width: 280px;
5
+ min-width: 0;
6
+ }
@@ -0,0 +1,28 @@
1
+ .providers-select {
2
+ max-width: 220px;
3
+ }
4
+
5
+ .providers-mix {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 10px;
9
+ padding: 12px 16px 14px;
10
+ background: var(--bg);
11
+ border-top: 1px solid var(--line-1);
12
+ }
13
+
14
+ /* Keeps account names aligned whether or not the row has a chart swatch. */
15
+ .providers-swatch-slot {
16
+ display: inline-flex;
17
+ width: 10px;
18
+ flex: none;
19
+ }
20
+
21
+ .providers-account {
22
+ max-width: 280px;
23
+ }
24
+
25
+ .providers-accounts {
26
+ margin: 0 -16px -16px;
27
+ border-top: 1px solid var(--line-1);
28
+ }
@@ -0,0 +1,14 @@
1
+ .tools-filter {
2
+ width: 200px;
3
+ }
4
+
5
+ .tools-name {
6
+ max-width: 220px;
7
+ }
8
+
9
+ .tools-share {
10
+ display: inline-block;
11
+ min-width: 32px;
12
+ margin-left: 8px;
13
+ text-align: right;
14
+ }