apteva 0.4.44 → 0.4.48

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 (83) hide show
  1. package/dist/{ActivityPage.c48n83h2.js → ActivityPage.sw9p594m.js} +1 -1
  2. package/dist/{ApiDocsPage.yzcxx5ax.js → ApiDocsPage.90e03bz7.js} +1 -1
  3. package/dist/App.0ws87fpx.js +53 -0
  4. package/dist/App.3vnrera5.js +4 -0
  5. package/dist/App.94x6mh7f.js +20 -0
  6. package/dist/{App.qzbx5wtj.js → App.9sryp183.js} +1 -1
  7. package/dist/App.d9tny4t0.js +221 -0
  8. package/dist/{App.r5serxkt.js → App.jhb45d7r.js} +1 -1
  9. package/dist/App.p7jjw1zf.js +4 -0
  10. package/dist/App.pfbdzrhh.js +4 -0
  11. package/dist/App.stgng5bx.js +13 -0
  12. package/dist/{App.152mbs1r.js → App.tm3k7h4b.js} +1 -1
  13. package/dist/App.vkg121c6.js +4 -0
  14. package/dist/App.wghtdzsk.js +1 -0
  15. package/dist/App.xf7wsckg.js +4 -0
  16. package/dist/App.xva0tfzh.js +4 -0
  17. package/dist/App.ysxy7akk.js +61 -0
  18. package/dist/App.yzkh4gq2.js +4 -0
  19. package/dist/ConnectionsPage.q5f9fd37.js +3 -0
  20. package/dist/McpPage.f3ccrezb.js +3 -0
  21. package/dist/SettingsPage.3sqx6wm4.js +3 -0
  22. package/dist/SkillsPage.whxnez67.js +3 -0
  23. package/dist/TasksPage.zp4jfevw.js +3 -0
  24. package/dist/TelemetryPage.a9fmxq87.js +3 -0
  25. package/dist/TestsPage.18krj0d1.js +3 -0
  26. package/dist/ThreadsPage.nnphgy98.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +5 -4
  31. package/src/db.ts +42 -4
  32. package/src/providers.ts +14 -9
  33. package/src/routes/api/agent-utils.ts +25 -3
  34. package/src/routes/api/telemetry.ts +20 -2
  35. package/src/server.ts +52 -1
  36. package/src/web/App.tsx +1 -1
  37. package/src/web/components/agents/AgentCard.tsx +9 -7
  38. package/src/web/components/agents/AgentPanel.tsx +205 -44
  39. package/src/web/components/agents/CreateAgentModal.tsx +5 -5
  40. package/src/web/components/auth/LoginPage.tsx +2 -2
  41. package/src/web/components/common/LoadingSpinner.tsx +1 -1
  42. package/src/web/components/common/Modal.tsx +6 -6
  43. package/src/web/components/common/Select.tsx +2 -2
  44. package/src/web/components/connections/ConnectionsPage.tsx +1 -1
  45. package/src/web/components/connections/IntegrationsTab.tsx +3 -3
  46. package/src/web/components/connections/OverviewTab.tsx +3 -3
  47. package/src/web/components/connections/TriggersTab.tsx +8 -8
  48. package/src/web/components/dashboard/Dashboard.tsx +2 -2
  49. package/src/web/components/layout/Header.tsx +3 -3
  50. package/src/web/components/layout/Sidebar.tsx +3 -2
  51. package/src/web/components/mcp/McpPage.tsx +13 -13
  52. package/src/web/components/onboarding/OnboardingWizard.tsx +2 -2
  53. package/src/web/components/settings/SettingsPage.tsx +55 -22
  54. package/src/web/components/skills/SkillsPage.tsx +7 -7
  55. package/src/web/components/tasks/TasksPage.tsx +212 -36
  56. package/src/web/components/telemetry/TelemetryPage.tsx +278 -9
  57. package/src/web/components/tests/TestsPage.tsx +2 -2
  58. package/src/web/components/threads/ThreadsPage.tsx +2 -2
  59. package/src/web/context/ThemeContext.tsx +31 -10
  60. package/src/web/index.html +1 -6
  61. package/src/web/styles.css +47 -0
  62. package/src/web/themes.ts +68 -5
  63. package/dist/App.09yb8t0b.js +0 -1
  64. package/dist/App.3a67nx9w.js +0 -4
  65. package/dist/App.9epx6785.js +0 -4
  66. package/dist/App.d8955awp.js +0 -4
  67. package/dist/App.drwb57jq.js +0 -4
  68. package/dist/App.gssbmajb.js +0 -4
  69. package/dist/App.qw70pc29.js +0 -53
  70. package/dist/App.tpmp9020.js +0 -20
  71. package/dist/App.v2wb4d7d.js +0 -61
  72. package/dist/App.vxmaaj0m.js +0 -13
  73. package/dist/App.w4p2tda9.js +0 -4
  74. package/dist/App.wv2ng55q.js +0 -221
  75. package/dist/App.yncnrn0f.js +0 -4
  76. package/dist/ConnectionsPage.k6cspyqq.js +0 -3
  77. package/dist/McpPage.cdxm48xj.js +0 -3
  78. package/dist/SettingsPage.evpv7c2y.js +0 -3
  79. package/dist/SkillsPage.pvzp6c1a.js +0 -3
  80. package/dist/TasksPage.6jnvbpsy.js +0 -3
  81. package/dist/TelemetryPage.t7vk24zc.js +0 -3
  82. package/dist/TestsPage.5x6658aa.js +0 -3
  83. package/dist/ThreadsPage.3fvhtevh.js +0 -3
@@ -13,6 +13,9 @@ interface TelemetryStats {
13
13
  total_errors: number;
14
14
  total_input_tokens: number;
15
15
  total_output_tokens: number;
16
+ total_cache_creation_tokens: number;
17
+ total_cache_read_tokens: number;
18
+ total_reasoning_tokens: number;
16
19
  total_cost: number;
17
20
  }
18
21
 
@@ -20,6 +23,22 @@ interface UsageByAgent {
20
23
  agent_id: string;
21
24
  input_tokens: number;
22
25
  output_tokens: number;
26
+ cache_creation_tokens: number;
27
+ cache_read_tokens: number;
28
+ reasoning_tokens: number;
29
+ llm_calls: number;
30
+ tool_calls: number;
31
+ errors: number;
32
+ cost: number;
33
+ }
34
+
35
+ interface UsageByProject {
36
+ project_id: string | null;
37
+ input_tokens: number;
38
+ output_tokens: number;
39
+ cache_creation_tokens: number;
40
+ cache_read_tokens: number;
41
+ reasoning_tokens: number;
23
42
  llm_calls: number;
24
43
  tool_calls: number;
25
44
  errors: number;
@@ -42,12 +61,18 @@ function extractEventStats(event: TelemetryEvent): {
42
61
  errors: number;
43
62
  input_tokens: number;
44
63
  output_tokens: number;
64
+ cache_creation_tokens: number;
65
+ cache_read_tokens: number;
66
+ reasoning_tokens: number;
45
67
  } {
46
68
  const isLlm = event.category === "LLM";
47
69
  const isTool = event.category === "TOOL";
48
70
  const isError = event.level === "error";
49
71
  const inputTokens = (event.data?.input_tokens as number) || 0;
50
72
  const outputTokens = (event.data?.output_tokens as number) || 0;
73
+ const cacheCreationTokens = (event.data?.cache_creation_tokens as number) || 0;
74
+ const cacheReadTokens = (event.data?.cache_read_tokens as number) || 0;
75
+ const reasoningTokens = (event.data?.reasoning_tokens as number) || 0;
51
76
 
52
77
  return {
53
78
  llm_calls: isLlm ? 1 : 0,
@@ -55,17 +80,21 @@ function extractEventStats(event: TelemetryEvent): {
55
80
  errors: isError ? 1 : 0,
56
81
  input_tokens: inputTokens,
57
82
  output_tokens: outputTokens,
83
+ cache_creation_tokens: cacheCreationTokens,
84
+ cache_read_tokens: cacheReadTokens,
85
+ reasoning_tokens: reasoningTokens,
58
86
  };
59
87
  }
60
88
 
61
89
  export function TelemetryPage() {
62
90
  const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
63
- const { currentProjectId, currentProject, costTrackingEnabled } = useProjects();
91
+ const { currentProjectId, currentProject, costTrackingEnabled, projectsEnabled, projects } = useProjects();
64
92
  const { authFetch } = useAuth();
65
93
  const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
66
94
  const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
67
95
  const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
68
96
  const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
97
+ const [projectUsage, setProjectUsage] = useState<UsageByProject[]>([]);
69
98
  const [loading, setLoading] = useState(true);
70
99
  const [filter, setFilter] = useState({
71
100
  level: "",
@@ -76,8 +105,11 @@ export function TelemetryPage() {
76
105
  const [agents, setAgents] = useState<Array<{ id: string; name: string; projectId: string | null }>>([]);
77
106
  const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
78
107
 
108
+ // Time range filter
109
+ const [timeRange, setTimeRange] = useState<string>("all");
110
+
79
111
  // Sort state for usage table
80
- type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "errors" | "cost";
112
+ type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
81
113
  const [sortKey, setSortKey] = useState<SortKey>("cost");
82
114
  const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
83
115
 
@@ -90,6 +122,20 @@ export function TelemetryPage() {
90
122
  }
91
123
  };
92
124
 
125
+ // Sort state for project usage table
126
+ type ProjectSortKey = "project" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
127
+ const [projectSortKey, setProjectSortKey] = useState<ProjectSortKey>("cost");
128
+ const [projectSortDir, setProjectSortDir] = useState<"asc" | "desc">("desc");
129
+
130
+ const handleProjectSort = (key: ProjectSortKey) => {
131
+ if (projectSortKey === key) {
132
+ setProjectSortDir(d => d === "asc" ? "desc" : "asc");
133
+ } else {
134
+ setProjectSortKey(key);
135
+ setProjectSortDir("desc");
136
+ }
137
+ };
138
+
93
139
  // Track IDs that were in the fetched stats to avoid double-counting
94
140
  const countedEventIdsRef = useRef<Set<string>>(new Set());
95
141
 
@@ -121,16 +167,35 @@ export function TelemetryPage() {
121
167
  // Get agent IDs for the current project
122
168
  const projectAgentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
123
169
 
170
+ // Compute `since` ISO string from selected time range
171
+ const getSince = useCallback((): string | undefined => {
172
+ if (timeRange === "all") return undefined;
173
+ const now = new Date();
174
+ const ms: Record<string, number> = {
175
+ "1h": 3600000,
176
+ "6h": 6 * 3600000,
177
+ "24h": 24 * 3600000,
178
+ "7d": 7 * 24 * 3600000,
179
+ "30d": 30 * 24 * 3600000,
180
+ };
181
+ if (ms[timeRange]) {
182
+ return new Date(now.getTime() - ms[timeRange]).toISOString();
183
+ }
184
+ return undefined;
185
+ }, [timeRange]);
186
+
124
187
  // Fetch stats and historical data (less frequently now since we have real-time)
125
188
  const fetchData = async () => {
126
189
  setLoading(true);
127
190
  try {
128
191
  // Build project filter param
129
192
  const projectParam = currentProjectId === "unassigned" ? "null" : currentProjectId || "";
193
+ const since = getSince();
130
194
 
131
195
  // Fetch stats
132
196
  const statsParams = new URLSearchParams();
133
197
  if (projectParam) statsParams.set("project_id", projectParam);
198
+ if (since) statsParams.set("since", since);
134
199
  const statsRes = await authFetch(`/api/telemetry/stats${statsParams.toString() ? `?${statsParams}` : ""}`);
135
200
  const statsData = await statsRes.json();
136
201
  setFetchedStats(statsData.stats);
@@ -140,6 +205,7 @@ export function TelemetryPage() {
140
205
  if (filter.level) params.set("level", filter.level);
141
206
  if (filter.agent_id) params.set("agent_id", filter.agent_id);
142
207
  if (projectParam) params.set("project_id", projectParam);
208
+ if (since) params.set("since", since);
143
209
  params.set("limit", "100"); // Fetch more since we filter client-side
144
210
 
145
211
  const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
@@ -154,6 +220,7 @@ export function TelemetryPage() {
154
220
  const usageParams = new URLSearchParams();
155
221
  usageParams.set("group_by", "agent");
156
222
  if (projectParam) usageParams.set("project_id", projectParam);
223
+ if (since) usageParams.set("since", since);
157
224
  const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
158
225
  const usageData = await usageRes.json();
159
226
  setFetchedUsage(usageData.usage || []);
@@ -162,6 +229,7 @@ export function TelemetryPage() {
162
229
  const dailyParams = new URLSearchParams();
163
230
  dailyParams.set("group_by", "day");
164
231
  if (projectParam) dailyParams.set("project_id", projectParam);
232
+ if (since) dailyParams.set("since", since);
165
233
  const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
166
234
  const dailyData = await dailyRes.json();
167
235
  // Sort by date ascending for charts
@@ -169,6 +237,18 @@ export function TelemetryPage() {
169
237
  a.date.localeCompare(b.date)
170
238
  );
171
239
  setDailyUsage(sorted);
240
+
241
+ // Fetch usage by project (only when viewing all projects and feature is enabled)
242
+ if (projectsEnabled && currentProjectId === null) {
243
+ const projParams = new URLSearchParams();
244
+ projParams.set("group_by", "project");
245
+ if (since) projParams.set("since", since);
246
+ const projRes = await authFetch(`/api/telemetry/usage?${projParams}`);
247
+ const projData = await projRes.json();
248
+ setProjectUsage(projData.usage || []);
249
+ } else {
250
+ setProjectUsage([]);
251
+ }
172
252
  } catch (e) {
173
253
  console.error("Failed to fetch telemetry:", e);
174
254
  }
@@ -177,7 +257,7 @@ export function TelemetryPage() {
177
257
 
178
258
  useEffect(() => {
179
259
  fetchData();
180
- }, [filter, currentProjectId, authFetch, statusChangeCounter]);
260
+ }, [filter, timeRange, currentProjectId, authFetch, statusChangeCounter]);
181
261
 
182
262
  // Compute real-time stats from new events (not already counted in fetched stats)
183
263
  const stats = useMemo(() => {
@@ -190,6 +270,9 @@ export function TelemetryPage() {
190
270
  let deltaErrors = 0;
191
271
  let deltaInputTokens = 0;
192
272
  let deltaOutputTokens = 0;
273
+ let deltaCacheCreationTokens = 0;
274
+ let deltaCacheReadTokens = 0;
275
+ let deltaReasoningTokens = 0;
193
276
 
194
277
  for (const event of realtimeEvents) {
195
278
  if (!countedEventIdsRef.current.has(event.id)) {
@@ -200,6 +283,9 @@ export function TelemetryPage() {
200
283
  deltaErrors += eventStats.errors;
201
284
  deltaInputTokens += eventStats.input_tokens;
202
285
  deltaOutputTokens += eventStats.output_tokens;
286
+ deltaCacheCreationTokens += eventStats.cache_creation_tokens;
287
+ deltaCacheReadTokens += eventStats.cache_read_tokens;
288
+ deltaReasoningTokens += eventStats.reasoning_tokens;
203
289
  }
204
290
  }
205
291
 
@@ -210,6 +296,9 @@ export function TelemetryPage() {
210
296
  total_errors: fetchedStats.total_errors + deltaErrors,
211
297
  total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
212
298
  total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
299
+ total_cache_creation_tokens: (fetchedStats.total_cache_creation_tokens || 0) + deltaCacheCreationTokens,
300
+ total_cache_read_tokens: (fetchedStats.total_cache_read_tokens || 0) + deltaCacheReadTokens,
301
+ total_reasoning_tokens: (fetchedStats.total_reasoning_tokens || 0) + deltaReasoningTokens,
213
302
  total_cost: fetchedStats.total_cost || 0,
214
303
  };
215
304
  }, [fetchedStats, realtimeEvents]);
@@ -233,6 +322,9 @@ export function TelemetryPage() {
233
322
  existing.errors += eventStats.errors;
234
323
  existing.input_tokens += eventStats.input_tokens;
235
324
  existing.output_tokens += eventStats.output_tokens;
325
+ existing.cache_creation_tokens += eventStats.cache_creation_tokens;
326
+ existing.cache_read_tokens += eventStats.cache_read_tokens;
327
+ existing.reasoning_tokens += eventStats.reasoning_tokens;
236
328
  } else {
237
329
  usageMap.set(event.agent_id, {
238
330
  agent_id: event.agent_id,
@@ -241,6 +333,9 @@ export function TelemetryPage() {
241
333
  errors: eventStats.errors,
242
334
  input_tokens: eventStats.input_tokens,
243
335
  output_tokens: eventStats.output_tokens,
336
+ cache_creation_tokens: eventStats.cache_creation_tokens,
337
+ cache_read_tokens: eventStats.cache_read_tokens,
338
+ reasoning_tokens: eventStats.reasoning_tokens,
244
339
  cost: 0,
245
340
  });
246
341
  }
@@ -266,6 +361,33 @@ export function TelemetryPage() {
266
361
  return sorted;
267
362
  }, [usage, sortKey, sortDir, agents]);
268
363
 
364
+ // Sorted project usage for the table
365
+ const sortedProjectUsage = useMemo(() => {
366
+ const sorted = [...projectUsage];
367
+ sorted.sort((a, b) => {
368
+ if (projectSortKey === "project") {
369
+ const aName = (a.project_id ? projects.find(p => p.id === a.project_id)?.name || a.project_id : "Unassigned").toLowerCase();
370
+ const bName = (b.project_id ? projects.find(p => p.id === b.project_id)?.name || b.project_id : "Unassigned").toLowerCase();
371
+ return projectSortDir === "asc" ? (aName < bName ? -1 : 1) : (aName > bName ? -1 : 1);
372
+ }
373
+ const aVal = a[projectSortKey] as number;
374
+ const bVal = b[projectSortKey] as number;
375
+ return projectSortDir === "asc" ? aVal - bVal : bVal - aVal;
376
+ });
377
+ return sorted;
378
+ }, [projectUsage, projectSortKey, projectSortDir, projects]);
379
+
380
+ const getProjectName = (projectId: string | null) => {
381
+ if (!projectId) return "Unassigned";
382
+ const project = projects.find(p => p.id === projectId);
383
+ return project?.name || projectId;
384
+ };
385
+
386
+ const getProjectColor = (projectId: string | null) => {
387
+ if (!projectId) return undefined;
388
+ return projects.find(p => p.id === projectId)?.color;
389
+ };
390
+
269
391
  // Merge real-time events with historical, filtering and deduping
270
392
  const allEvents = React.useMemo(() => {
271
393
  // Apply filters to real-time events
@@ -419,15 +541,48 @@ export function TelemetryPage() {
419
541
  </p>
420
542
  </div>
421
543
 
544
+ {/* Time Range Selector */}
545
+ <div className="flex gap-2 mb-6">
546
+ {[
547
+ { value: "1h", label: "Last hour" },
548
+ { value: "6h", label: "Last 6h" },
549
+ { value: "24h", label: "Last 24h" },
550
+ { value: "7d", label: "Last 7 days" },
551
+ { value: "30d", label: "Last 30 days" },
552
+ { value: "all", label: "All time" },
553
+ ].map(opt => (
554
+ <button
555
+ key={opt.value}
556
+ onClick={() => setTimeRange(opt.value)}
557
+ className={`px-3 py-1.5 rounded text-sm transition ${
558
+ timeRange === opt.value
559
+ ? "bg-[var(--color-accent)] text-black"
560
+ : "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)]"
561
+ }`}
562
+ >
563
+ {opt.label}
564
+ </button>
565
+ ))}
566
+ </div>
567
+
422
568
  {/* Stats Cards */}
423
569
  {stats && (
424
- <div className={`grid grid-cols-2 md:grid-cols-3 ${costTrackingEnabled ? "lg:grid-cols-7" : "lg:grid-cols-6"} gap-4 mb-6`}>
570
+ <div className="flex flex-wrap gap-4 mb-6">
425
571
  <StatCard label="Events" value={formatNumber(stats.total_events)} />
426
572
  <StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
427
573
  <StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />
428
574
  <StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
429
575
  <StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
430
576
  <StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
577
+ {(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
578
+ <>
579
+ <StatCard label="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
580
+ <StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
581
+ </>
582
+ )}
583
+ {stats.total_reasoning_tokens > 0 && (
584
+ <StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
585
+ )}
431
586
  {costTrackingEnabled && (
432
587
  <StatCard label="Total Cost" value={`$${stats.total_cost.toFixed(4)}`} color="orange" />
433
588
  )}
@@ -464,7 +619,7 @@ export function TelemetryPage() {
464
619
  return (
465
620
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
466
621
  {/* Activity Chart */}
467
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
622
+ <div className="bg-[var(--color-surface)] card p-4">
468
623
  <h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Activity</h3>
469
624
  <ResponsiveContainer width="100%" height={200}>
470
625
  <AreaChart data={chartData}>
@@ -506,6 +661,7 @@ export function TelemetryPage() {
506
661
  fill="var(--color-accent)"
507
662
  fillOpacity={0.15}
508
663
  strokeWidth={1.5}
664
+
509
665
  />
510
666
  <Area
511
667
  type="monotone"
@@ -515,6 +671,7 @@ export function TelemetryPage() {
515
671
  fill="var(--color-accent-hover)"
516
672
  fillOpacity={0.08}
517
673
  strokeWidth={1.5}
674
+
518
675
  />
519
676
  <Area
520
677
  type="monotone"
@@ -524,13 +681,14 @@ export function TelemetryPage() {
524
681
  fill="#ef4444"
525
682
  fillOpacity={0.1}
526
683
  strokeWidth={1.5}
684
+
527
685
  />
528
686
  </AreaChart>
529
687
  </ResponsiveContainer>
530
688
  </div>
531
689
 
532
690
  {/* Token Usage Chart */}
533
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
691
+ <div className="bg-[var(--color-surface)] card p-4">
534
692
  <h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Token Usage</h3>
535
693
  <ResponsiveContainer width="100%" height={200}>
536
694
  <BarChart data={chartData}>
@@ -578,12 +736,14 @@ export function TelemetryPage() {
578
736
  name="Input Tokens"
579
737
  fill="var(--color-accent)"
580
738
  radius={[2, 2, 0, 0]}
739
+
581
740
  />
582
741
  <Bar
583
742
  dataKey="output_tokens"
584
743
  name="Output Tokens"
585
744
  fill="var(--color-accent-hover)"
586
745
  radius={[2, 2, 0, 0]}
746
+
587
747
  />
588
748
  </BarChart>
589
749
  </ResponsiveContainer>
@@ -595,6 +755,8 @@ export function TelemetryPage() {
595
755
  {/* Usage by Agent */}
596
756
  {usage.length > 0 && (() => {
597
757
  const maxCost = Math.max(...sortedUsage.map(u => u.cost), 0.0001);
758
+ const hasCacheTokens = sortedUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
759
+ const hasReasoningTokens = sortedUsage.some(u => (u.reasoning_tokens || 0) > 0);
598
760
  const SortHeader = ({ label, field, align = "right" }: { label: string; field: SortKey; align?: string }) => (
599
761
  <th
600
762
  className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
@@ -615,7 +777,7 @@ export function TelemetryPage() {
615
777
  return (
616
778
  <div className="mb-6">
617
779
  <h2 className="text-lg font-medium mb-3">Usage by Agent</h2>
618
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg overflow-hidden">
780
+ <div className="bg-[var(--color-surface)] card overflow-hidden">
619
781
  <table className="w-full text-sm">
620
782
  <thead>
621
783
  <tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
@@ -624,6 +786,9 @@ export function TelemetryPage() {
624
786
  <SortHeader label="Tool Calls" field="tool_calls" />
625
787
  <SortHeader label="Input Tokens" field="input_tokens" />
626
788
  <SortHeader label="Output Tokens" field="output_tokens" />
789
+ {hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
790
+ {hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
791
+ {hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
627
792
  <SortHeader label="Errors" field="errors" />
628
793
  {costTrackingEnabled && <SortHeader label="Est. Cost" field="cost" />}
629
794
  </tr>
@@ -636,6 +801,15 @@ export function TelemetryPage() {
636
801
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
637
802
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
638
803
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
804
+ {hasCacheTokens && (
805
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
806
+ )}
807
+ {hasCacheTokens && (
808
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
809
+ )}
810
+ {hasReasoningTokens && (
811
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
812
+ )}
639
813
  <td className="p-3 text-right">
640
814
  {u.errors > 0 ? (
641
815
  <span className="text-red-400">{u.errors}</span>
@@ -665,6 +839,101 @@ export function TelemetryPage() {
665
839
  );
666
840
  })()}
667
841
 
842
+ {/* Usage by Project */}
843
+ {projectsEnabled && currentProjectId === null && sortedProjectUsage.length > 0 && (() => {
844
+ const maxCost = Math.max(...sortedProjectUsage.map(u => u.cost), 0.0001);
845
+ const hasProjCacheTokens = sortedProjectUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
846
+ const hasProjReasoningTokens = sortedProjectUsage.some(u => (u.reasoning_tokens || 0) > 0);
847
+ const PSortHeader = ({ label, field, align = "right" }: { label: string; field: ProjectSortKey; align?: string }) => (
848
+ <th
849
+ className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
850
+ onClick={() => handleProjectSort(field)}
851
+ >
852
+ <span className="inline-flex items-center gap-1">
853
+ {align === "right" && projectSortKey === field && (
854
+ <span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
855
+ )}
856
+ {label}
857
+ {align === "left" && projectSortKey === field && (
858
+ <span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
859
+ )}
860
+ </span>
861
+ </th>
862
+ );
863
+
864
+ return (
865
+ <div className="mb-6">
866
+ <h2 className="text-lg font-medium mb-3">Usage by Project</h2>
867
+ <div className="bg-[var(--color-surface)] card overflow-hidden">
868
+ <table className="w-full text-sm">
869
+ <thead>
870
+ <tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
871
+ <PSortHeader label="Project" field="project" align="left" />
872
+ <PSortHeader label="LLM Calls" field="llm_calls" />
873
+ <PSortHeader label="Tool Calls" field="tool_calls" />
874
+ <PSortHeader label="Input Tokens" field="input_tokens" />
875
+ <PSortHeader label="Output Tokens" field="output_tokens" />
876
+ {hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
877
+ {hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
878
+ {hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
879
+ <PSortHeader label="Errors" field="errors" />
880
+ {costTrackingEnabled && <PSortHeader label="Est. Cost" field="cost" />}
881
+ </tr>
882
+ </thead>
883
+ <tbody>
884
+ {sortedProjectUsage.map((u) => {
885
+ const color = getProjectColor(u.project_id);
886
+ return (
887
+ <tr key={u.project_id || "_unassigned"} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
888
+ <td className="p-3 font-medium">
889
+ <span className="inline-flex items-center gap-2">
890
+ {color && <span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />}
891
+ {getProjectName(u.project_id)}
892
+ </span>
893
+ </td>
894
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
895
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
896
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
897
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
898
+ {hasProjCacheTokens && (
899
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
900
+ )}
901
+ {hasProjCacheTokens && (
902
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
903
+ )}
904
+ {hasProjReasoningTokens && (
905
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
906
+ )}
907
+ <td className="p-3 text-right">
908
+ {u.errors > 0 ? (
909
+ <span className="text-red-400">{u.errors}</span>
910
+ ) : (
911
+ <span className="text-[var(--color-text-faint)]">0</span>
912
+ )}
913
+ </td>
914
+ {costTrackingEnabled && (
915
+ <td className="p-3 text-right">
916
+ <div className="flex items-center justify-end gap-2">
917
+ <div className="w-16 h-1.5 bg-[var(--color-surface-raised)] rounded-full overflow-hidden">
918
+ <div
919
+ className="h-full bg-orange-500 rounded-full"
920
+ style={{ width: `${(u.cost / maxCost) * 100}%` }}
921
+ />
922
+ </div>
923
+ <span className="text-[var(--color-text-secondary)] min-w-[60px] text-right">${u.cost.toFixed(4)}</span>
924
+ </div>
925
+ </td>
926
+ )}
927
+ </tr>
928
+ );
929
+ })}
930
+ </tbody>
931
+ </table>
932
+ </div>
933
+ </div>
934
+ );
935
+ })()}
936
+
668
937
  {/* Filters */}
669
938
  <div className="flex flex-wrap items-center gap-3 mb-4">
670
939
  <div className="w-44">
@@ -714,7 +983,7 @@ export function TelemetryPage() {
714
983
  </div>
715
984
 
716
985
  {/* Events List */}
717
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg">
986
+ <div className="bg-[var(--color-surface)] card">
718
987
  <div className="p-3 border-b border-[var(--color-border)] flex items-center justify-between">
719
988
  <h2 className="font-medium">Recent Events</h2>
720
989
  {realtimeEvents.length > 0 && (
@@ -791,7 +1060,7 @@ export function TelemetryPage() {
791
1060
 
792
1061
  function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
793
1062
  return (
794
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
1063
+ <div className="bg-[var(--color-surface)] card p-4 flex-1 min-w-[120px]">
795
1064
  <div className="text-[var(--color-text-muted)] text-xs mb-1">{label}</div>
796
1065
  <div className={`text-2xl font-semibold ${color === "red" ? "text-red-400" : color === "orange" ? "text-orange-400" : ""}`}>
797
1066
  {value}
@@ -342,7 +342,7 @@ export function TestsPage() {
342
342
  ) : (
343
343
  <div className="space-y-3">
344
344
  {tests.map(tc => (
345
- <div key={tc.id} className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
345
+ <div key={tc.id} className="bg-[var(--color-surface)] card p-4">
346
346
  <div className="flex items-start justify-between">
347
347
  <div className="flex-1 min-w-0">
348
348
  <div className="flex items-center gap-2 mb-1">
@@ -515,7 +515,7 @@ export function TestsPage() {
515
515
  {/* Create/Edit Form Modal */}
516
516
  {showForm && (
517
517
  <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowForm(false)}>
518
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
518
+ <div className="bg-[var(--color-surface)] card w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
519
519
  <h2 className="text-lg font-bold mb-4">{editingTest ? "Edit Test" : "New Test"}</h2>
520
520
 
521
521
  <div className="space-y-4">
@@ -163,7 +163,7 @@ export function ThreadsPage({ agents, onNavigate }: ThreadsPageProps) {
163
163
  <button
164
164
  onClick={() => setShowAgentPicker(!showAgentPicker)}
165
165
  disabled={runningAgents.length === 0}
166
- className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[var(--color-accent-10)] text-[var(--color-accent)] text-sm font-medium hover:bg-[var(--color-accent-20)] transition disabled:opacity-30 disabled:cursor-not-allowed"
166
+ className="w-full flex items-center justify-center gap-2 px-3 py-2 btn bg-[var(--color-accent-10)] text-[var(--color-accent)] text-sm font-medium hover:bg-[var(--color-accent-20)] transition disabled:opacity-30 disabled:cursor-not-allowed"
167
167
  >
168
168
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
169
169
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
@@ -175,7 +175,7 @@ export function ThreadsPage({ agents, onNavigate }: ThreadsPageProps) {
175
175
  {showAgentPicker && (
176
176
  <>
177
177
  <div className="fixed inset-0 z-40" onClick={() => setShowAgentPicker(false)} />
178
- <div className="absolute top-full left-0 right-0 mt-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg shadow-xl z-50 max-h-60 overflow-auto">
178
+ <div className="absolute top-full left-0 right-0 mt-1 bg-[var(--color-surface)] card shadow-xl z-50 max-h-60 overflow-auto">
179
179
  {runningAgents.map(agent => (
180
180
  <button
181
181
  key={agent.id}