apteva 0.4.44 → 0.4.51

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 (85) 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.3vnrera5.js +4 -0
  4. package/dist/App.94x6mh7f.js +20 -0
  5. package/dist/{App.qzbx5wtj.js → App.9sryp183.js} +1 -1
  6. package/dist/App.9t1zc5r7.js +53 -0
  7. package/dist/{App.r5serxkt.js → App.jhb45d7r.js} +1 -1
  8. package/dist/App.k4nmqgek.js +221 -0
  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.vza4fxg0.js +4 -0
  15. package/dist/App.wghtdzsk.js +1 -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.q1pqcc93.js +3 -0
  22. package/dist/SkillsPage.whxnez67.js +3 -0
  23. package/dist/TasksPage.zp4jfevw.js +3 -0
  24. package/dist/TelemetryPage.an0ky78c.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 +10 -9
  31. package/src/db.ts +60 -22
  32. package/src/providers.ts +14 -9
  33. package/src/routes/api/agent-utils.ts +25 -3
  34. package/src/routes/api/telemetry.ts +21 -2
  35. package/src/server.ts +53 -1
  36. package/src/web/App.tsx +2 -2
  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 +4 -4
  49. package/src/web/components/layout/Header.tsx +3 -3
  50. package/src/web/components/layout/Sidebar.tsx +6 -5
  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 +59 -26
  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 +414 -94
  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/TelemetryContext.tsx +1 -0
  60. package/src/web/context/ThemeContext.tsx +31 -10
  61. package/src/web/index.html +1 -6
  62. package/src/web/styles.css +47 -0
  63. package/src/web/themes.ts +68 -5
  64. package/src/web/types.ts +1 -1
  65. package/dist/App.09yb8t0b.js +0 -1
  66. package/dist/App.3a67nx9w.js +0 -4
  67. package/dist/App.9epx6785.js +0 -4
  68. package/dist/App.d8955awp.js +0 -4
  69. package/dist/App.drwb57jq.js +0 -4
  70. package/dist/App.gssbmajb.js +0 -4
  71. package/dist/App.qw70pc29.js +0 -53
  72. package/dist/App.tpmp9020.js +0 -20
  73. package/dist/App.v2wb4d7d.js +0 -61
  74. package/dist/App.vxmaaj0m.js +0 -13
  75. package/dist/App.w4p2tda9.js +0 -4
  76. package/dist/App.wv2ng55q.js +0 -221
  77. package/dist/App.yncnrn0f.js +0 -4
  78. package/dist/ConnectionsPage.k6cspyqq.js +0 -3
  79. package/dist/McpPage.cdxm48xj.js +0 -3
  80. package/dist/SettingsPage.evpv7c2y.js +0 -3
  81. package/dist/SkillsPage.pvzp6c1a.js +0 -3
  82. package/dist/TasksPage.6jnvbpsy.js +0 -3
  83. package/dist/TelemetryPage.t7vk24zc.js +0 -3
  84. package/dist/TestsPage.5x6658aa.js +0 -3
  85. 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;
@@ -30,9 +49,13 @@ interface DailyUsage {
30
49
  date: string;
31
50
  input_tokens: number;
32
51
  output_tokens: number;
52
+ cache_creation_tokens: number;
53
+ cache_read_tokens: number;
54
+ reasoning_tokens: number;
33
55
  llm_calls: number;
34
56
  tool_calls: number;
35
57
  errors: number;
58
+ cost: number;
36
59
  }
37
60
 
38
61
  // Helper to extract stats from a single event
@@ -42,12 +65,20 @@ function extractEventStats(event: TelemetryEvent): {
42
65
  errors: number;
43
66
  input_tokens: number;
44
67
  output_tokens: number;
68
+ cache_creation_tokens: number;
69
+ cache_read_tokens: number;
70
+ reasoning_tokens: number;
71
+ cost: number;
45
72
  } {
46
73
  const isLlm = event.category === "LLM";
47
74
  const isTool = event.category === "TOOL";
48
75
  const isError = event.level === "error";
49
76
  const inputTokens = (event.data?.input_tokens as number) || 0;
50
77
  const outputTokens = (event.data?.output_tokens as number) || 0;
78
+ const cacheCreationTokens = (event.data?.cache_creation_tokens as number) || 0;
79
+ const cacheReadTokens = (event.data?.cache_read_tokens as number) || 0;
80
+ const reasoningTokens = (event.data?.reasoning_tokens as number) || 0;
81
+ const cost = (event.cost as number) || 0;
51
82
 
52
83
  return {
53
84
  llm_calls: isLlm ? 1 : 0,
@@ -55,17 +86,22 @@ function extractEventStats(event: TelemetryEvent): {
55
86
  errors: isError ? 1 : 0,
56
87
  input_tokens: inputTokens,
57
88
  output_tokens: outputTokens,
89
+ cache_creation_tokens: cacheCreationTokens,
90
+ cache_read_tokens: cacheReadTokens,
91
+ reasoning_tokens: reasoningTokens,
92
+ cost,
58
93
  };
59
94
  }
60
95
 
61
96
  export function TelemetryPage() {
62
97
  const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
63
- const { currentProjectId, currentProject, costTrackingEnabled } = useProjects();
98
+ const { currentProjectId, currentProject, costTrackingEnabled, projectsEnabled, projects } = useProjects();
64
99
  const { authFetch } = useAuth();
65
100
  const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
66
101
  const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
67
102
  const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
68
103
  const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
104
+ const [projectUsage, setProjectUsage] = useState<UsageByProject[]>([]);
69
105
  const [loading, setLoading] = useState(true);
70
106
  const [filter, setFilter] = useState({
71
107
  level: "",
@@ -76,8 +112,11 @@ export function TelemetryPage() {
76
112
  const [agents, setAgents] = useState<Array<{ id: string; name: string; projectId: string | null }>>([]);
77
113
  const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
78
114
 
115
+ // Time range filter
116
+ const [timeRange, setTimeRange] = useState<string>("all");
117
+
79
118
  // Sort state for usage table
80
- type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "errors" | "cost";
119
+ type SortKey = "agent" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
81
120
  const [sortKey, setSortKey] = useState<SortKey>("cost");
82
121
  const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
83
122
 
@@ -90,6 +129,20 @@ export function TelemetryPage() {
90
129
  }
91
130
  };
92
131
 
132
+ // Sort state for project usage table
133
+ type ProjectSortKey = "project" | "llm_calls" | "tool_calls" | "input_tokens" | "output_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "reasoning_tokens" | "errors" | "cost";
134
+ const [projectSortKey, setProjectSortKey] = useState<ProjectSortKey>("cost");
135
+ const [projectSortDir, setProjectSortDir] = useState<"asc" | "desc">("desc");
136
+
137
+ const handleProjectSort = (key: ProjectSortKey) => {
138
+ if (projectSortKey === key) {
139
+ setProjectSortDir(d => d === "asc" ? "desc" : "asc");
140
+ } else {
141
+ setProjectSortKey(key);
142
+ setProjectSortDir("desc");
143
+ }
144
+ };
145
+
93
146
  // Track IDs that were in the fetched stats to avoid double-counting
94
147
  const countedEventIdsRef = useRef<Set<string>>(new Set());
95
148
 
@@ -121,16 +174,35 @@ export function TelemetryPage() {
121
174
  // Get agent IDs for the current project
122
175
  const projectAgentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
123
176
 
177
+ // Compute `since` ISO string from selected time range
178
+ const getSince = useCallback((): string | undefined => {
179
+ if (timeRange === "all") return undefined;
180
+ const now = new Date();
181
+ const ms: Record<string, number> = {
182
+ "1h": 3600000,
183
+ "6h": 6 * 3600000,
184
+ "24h": 24 * 3600000,
185
+ "7d": 7 * 24 * 3600000,
186
+ "30d": 30 * 24 * 3600000,
187
+ };
188
+ if (ms[timeRange]) {
189
+ return new Date(now.getTime() - ms[timeRange]).toISOString();
190
+ }
191
+ return undefined;
192
+ }, [timeRange]);
193
+
124
194
  // Fetch stats and historical data (less frequently now since we have real-time)
125
195
  const fetchData = async () => {
126
196
  setLoading(true);
127
197
  try {
128
198
  // Build project filter param
129
199
  const projectParam = currentProjectId === "unassigned" ? "null" : currentProjectId || "";
200
+ const since = getSince();
130
201
 
131
202
  // Fetch stats
132
203
  const statsParams = new URLSearchParams();
133
204
  if (projectParam) statsParams.set("project_id", projectParam);
205
+ if (since) statsParams.set("since", since);
134
206
  const statsRes = await authFetch(`/api/telemetry/stats${statsParams.toString() ? `?${statsParams}` : ""}`);
135
207
  const statsData = await statsRes.json();
136
208
  setFetchedStats(statsData.stats);
@@ -140,6 +212,7 @@ export function TelemetryPage() {
140
212
  if (filter.level) params.set("level", filter.level);
141
213
  if (filter.agent_id) params.set("agent_id", filter.agent_id);
142
214
  if (projectParam) params.set("project_id", projectParam);
215
+ if (since) params.set("since", since);
143
216
  params.set("limit", "100"); // Fetch more since we filter client-side
144
217
 
145
218
  const eventsRes = await authFetch(`/api/telemetry/events?${params}`);
@@ -154,6 +227,7 @@ export function TelemetryPage() {
154
227
  const usageParams = new URLSearchParams();
155
228
  usageParams.set("group_by", "agent");
156
229
  if (projectParam) usageParams.set("project_id", projectParam);
230
+ if (since) usageParams.set("since", since);
157
231
  const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
158
232
  const usageData = await usageRes.json();
159
233
  setFetchedUsage(usageData.usage || []);
@@ -162,6 +236,7 @@ export function TelemetryPage() {
162
236
  const dailyParams = new URLSearchParams();
163
237
  dailyParams.set("group_by", "day");
164
238
  if (projectParam) dailyParams.set("project_id", projectParam);
239
+ if (since) dailyParams.set("since", since);
165
240
  const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
166
241
  const dailyData = await dailyRes.json();
167
242
  // Sort by date ascending for charts
@@ -169,6 +244,18 @@ export function TelemetryPage() {
169
244
  a.date.localeCompare(b.date)
170
245
  );
171
246
  setDailyUsage(sorted);
247
+
248
+ // Fetch usage by project (only when viewing all projects and feature is enabled)
249
+ if (projectsEnabled && currentProjectId === null) {
250
+ const projParams = new URLSearchParams();
251
+ projParams.set("group_by", "project");
252
+ if (since) projParams.set("since", since);
253
+ const projRes = await authFetch(`/api/telemetry/usage?${projParams}`);
254
+ const projData = await projRes.json();
255
+ setProjectUsage(projData.usage || []);
256
+ } else {
257
+ setProjectUsage([]);
258
+ }
172
259
  } catch (e) {
173
260
  console.error("Failed to fetch telemetry:", e);
174
261
  }
@@ -177,7 +264,7 @@ export function TelemetryPage() {
177
264
 
178
265
  useEffect(() => {
179
266
  fetchData();
180
- }, [filter, currentProjectId, authFetch, statusChangeCounter]);
267
+ }, [filter, timeRange, currentProjectId, authFetch, statusChangeCounter]);
181
268
 
182
269
  // Compute real-time stats from new events (not already counted in fetched stats)
183
270
  const stats = useMemo(() => {
@@ -190,6 +277,10 @@ export function TelemetryPage() {
190
277
  let deltaErrors = 0;
191
278
  let deltaInputTokens = 0;
192
279
  let deltaOutputTokens = 0;
280
+ let deltaCacheCreationTokens = 0;
281
+ let deltaCacheReadTokens = 0;
282
+ let deltaReasoningTokens = 0;
283
+ let deltaCost = 0;
193
284
 
194
285
  for (const event of realtimeEvents) {
195
286
  if (!countedEventIdsRef.current.has(event.id)) {
@@ -200,6 +291,10 @@ export function TelemetryPage() {
200
291
  deltaErrors += eventStats.errors;
201
292
  deltaInputTokens += eventStats.input_tokens;
202
293
  deltaOutputTokens += eventStats.output_tokens;
294
+ deltaCacheCreationTokens += eventStats.cache_creation_tokens;
295
+ deltaCacheReadTokens += eventStats.cache_read_tokens;
296
+ deltaReasoningTokens += eventStats.reasoning_tokens;
297
+ deltaCost += eventStats.cost;
203
298
  }
204
299
  }
205
300
 
@@ -210,7 +305,10 @@ export function TelemetryPage() {
210
305
  total_errors: fetchedStats.total_errors + deltaErrors,
211
306
  total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
212
307
  total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
213
- total_cost: fetchedStats.total_cost || 0,
308
+ total_cache_creation_tokens: (fetchedStats.total_cache_creation_tokens || 0) + deltaCacheCreationTokens,
309
+ total_cache_read_tokens: (fetchedStats.total_cache_read_tokens || 0) + deltaCacheReadTokens,
310
+ total_reasoning_tokens: (fetchedStats.total_reasoning_tokens || 0) + deltaReasoningTokens,
311
+ total_cost: (fetchedStats.total_cost || 0) + deltaCost,
214
312
  };
215
313
  }, [fetchedStats, realtimeEvents]);
216
314
 
@@ -233,6 +331,10 @@ export function TelemetryPage() {
233
331
  existing.errors += eventStats.errors;
234
332
  existing.input_tokens += eventStats.input_tokens;
235
333
  existing.output_tokens += eventStats.output_tokens;
334
+ existing.cache_creation_tokens += eventStats.cache_creation_tokens;
335
+ existing.cache_read_tokens += eventStats.cache_read_tokens;
336
+ existing.reasoning_tokens += eventStats.reasoning_tokens;
337
+ existing.cost += eventStats.cost;
236
338
  } else {
237
339
  usageMap.set(event.agent_id, {
238
340
  agent_id: event.agent_id,
@@ -241,7 +343,10 @@ export function TelemetryPage() {
241
343
  errors: eventStats.errors,
242
344
  input_tokens: eventStats.input_tokens,
243
345
  output_tokens: eventStats.output_tokens,
244
- cost: 0,
346
+ cache_creation_tokens: eventStats.cache_creation_tokens,
347
+ cache_read_tokens: eventStats.cache_read_tokens,
348
+ reasoning_tokens: eventStats.reasoning_tokens,
349
+ cost: eventStats.cost,
245
350
  });
246
351
  }
247
352
  }
@@ -266,6 +371,70 @@ export function TelemetryPage() {
266
371
  return sorted;
267
372
  }, [usage, sortKey, sortDir, agents]);
268
373
 
374
+ // Sorted project usage for the table
375
+ const sortedProjectUsage = useMemo(() => {
376
+ const sorted = [...projectUsage];
377
+ sorted.sort((a, b) => {
378
+ if (projectSortKey === "project") {
379
+ const aName = (a.project_id ? projects.find(p => p.id === a.project_id)?.name || a.project_id : "Unassigned").toLowerCase();
380
+ const bName = (b.project_id ? projects.find(p => p.id === b.project_id)?.name || b.project_id : "Unassigned").toLowerCase();
381
+ return projectSortDir === "asc" ? (aName < bName ? -1 : 1) : (aName > bName ? -1 : 1);
382
+ }
383
+ const aVal = a[projectSortKey] as number;
384
+ const bVal = b[projectSortKey] as number;
385
+ return projectSortDir === "asc" ? aVal - bVal : bVal - aVal;
386
+ });
387
+ return sorted;
388
+ }, [projectUsage, projectSortKey, projectSortDir, projects]);
389
+
390
+ // Compute real-time chart data by merging fetched daily usage with SSE deltas
391
+ const chartData = useMemo(() => {
392
+ const buckets = new Map<string, DailyUsage>();
393
+ const useDaily = dailyUsage.length > 1;
394
+
395
+ // Seed with fetched daily data
396
+ for (const d of dailyUsage) {
397
+ buckets.set(d.date, { ...d });
398
+ }
399
+
400
+ // Add deltas from real-time events not already counted
401
+ for (const event of realtimeEvents) {
402
+ if (!countedEventIdsRef.current.has(event.id)) {
403
+ const ts = new Date(event.timestamp);
404
+ const key = useDaily
405
+ ? `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")}`
406
+ : `${ts.getFullYear()}-${String(ts.getMonth() + 1).padStart(2, "0")}-${String(ts.getDate()).padStart(2, "0")} ${String(ts.getHours()).padStart(2, "0")}:00`;
407
+ if (!buckets.has(key)) {
408
+ buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, reasoning_tokens: 0, cost: 0 });
409
+ }
410
+ const b = buckets.get(key)!;
411
+ const s = extractEventStats(event);
412
+ b.llm_calls += s.llm_calls;
413
+ b.tool_calls += s.tool_calls;
414
+ b.errors += s.errors;
415
+ b.input_tokens += s.input_tokens;
416
+ b.output_tokens += s.output_tokens;
417
+ b.cache_creation_tokens += s.cache_creation_tokens;
418
+ b.cache_read_tokens += s.cache_read_tokens;
419
+ b.reasoning_tokens += s.reasoning_tokens;
420
+ b.cost += s.cost;
421
+ }
422
+ }
423
+
424
+ return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
425
+ }, [dailyUsage, realtimeEvents]);
426
+
427
+ const getProjectName = (projectId: string | null) => {
428
+ if (!projectId) return "Unassigned";
429
+ const project = projects.find(p => p.id === projectId);
430
+ return project?.name || projectId;
431
+ };
432
+
433
+ const getProjectColor = (projectId: string | null) => {
434
+ if (!projectId) return undefined;
435
+ return projects.find(p => p.id === projectId)?.color;
436
+ };
437
+
269
438
  // Merge real-time events with historical, filtering and deduping
270
439
  const allEvents = React.useMemo(() => {
271
440
  // Apply filters to real-time events
@@ -408,26 +577,59 @@ export function TelemetryPage() {
408
577
  )}
409
578
  <h1 className="text-2xl font-semibold">
410
579
  {currentProjectId === null
411
- ? "Telemetry"
580
+ ? "Analytics"
412
581
  : currentProjectId === "unassigned"
413
- ? "Telemetry - Unassigned"
414
- : `Telemetry - ${currentProject?.name || ""}`}
582
+ ? "Analytics - Unassigned"
583
+ : `Analytics - ${currentProject?.name || ""}`}
415
584
  </h1>
416
585
  </div>
417
586
  <p className="text-[var(--color-text-muted)]">
418
- Monitor agent activity, token usage, and errors.
587
+ Monitor agent activity, usage, costs, and performance.
419
588
  </p>
420
589
  </div>
421
590
 
591
+ {/* Time Range Selector */}
592
+ <div className="flex gap-2 mb-6">
593
+ {[
594
+ { value: "1h", label: "Last hour" },
595
+ { value: "6h", label: "Last 6h" },
596
+ { value: "24h", label: "Last 24h" },
597
+ { value: "7d", label: "Last 7 days" },
598
+ { value: "30d", label: "Last 30 days" },
599
+ { value: "all", label: "All time" },
600
+ ].map(opt => (
601
+ <button
602
+ key={opt.value}
603
+ onClick={() => setTimeRange(opt.value)}
604
+ className={`px-3 py-1.5 rounded text-sm transition ${
605
+ timeRange === opt.value
606
+ ? "bg-[var(--color-accent)] text-black"
607
+ : "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)]"
608
+ }`}
609
+ >
610
+ {opt.label}
611
+ </button>
612
+ ))}
613
+ </div>
614
+
422
615
  {/* Stats Cards */}
423
616
  {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`}>
617
+ <div className="flex flex-wrap gap-4 mb-6">
425
618
  <StatCard label="Events" value={formatNumber(stats.total_events)} />
426
619
  <StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
427
620
  <StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />
428
621
  <StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
429
622
  <StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
430
623
  <StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
624
+ {(stats.total_cache_creation_tokens > 0 || stats.total_cache_read_tokens > 0) && (
625
+ <>
626
+ <StatCard label="Cache Write" value={formatNumber(stats.total_cache_creation_tokens)} />
627
+ <StatCard label="Cache Read" value={formatNumber(stats.total_cache_read_tokens)} />
628
+ </>
629
+ )}
630
+ {stats.total_reasoning_tokens > 0 && (
631
+ <StatCard label="Reasoning" value={formatNumber(stats.total_reasoning_tokens)} />
632
+ )}
431
633
  {costTrackingEnabled && (
432
634
  <StatCard label="Total Cost" value={`$${stats.total_cost.toFixed(4)}`} color="orange" />
433
635
  )}
@@ -436,27 +638,7 @@ export function TelemetryPage() {
436
638
 
437
639
  {/* Charts */}
438
640
  {(() => {
439
- // Use daily data if we have multiple days, otherwise aggregate events by hour
440
641
  const useDaily = dailyUsage.length > 1;
441
- const chartData = useDaily ? dailyUsage : (() => {
442
- // Aggregate all visible events by hour
443
- const buckets = new Map<string, { date: string; llm_calls: number; tool_calls: number; errors: number; input_tokens: number; output_tokens: number }>();
444
- for (const event of allEvents) {
445
- const d = new Date(event.timestamp);
446
- const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
447
- if (!buckets.has(key)) {
448
- buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0 });
449
- }
450
- const b = buckets.get(key)!;
451
- const s = extractEventStats(event);
452
- b.llm_calls += s.llm_calls;
453
- b.tool_calls += s.tool_calls;
454
- b.errors += s.errors;
455
- b.input_tokens += s.input_tokens;
456
- b.output_tokens += s.output_tokens;
457
- }
458
- return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
459
- })();
460
642
  const chartLabel = useDaily ? "Daily" : "Hourly";
461
643
 
462
644
  if (chartData.length === 0) return null;
@@ -464,7 +646,7 @@ export function TelemetryPage() {
464
646
  return (
465
647
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
466
648
  {/* Activity Chart */}
467
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
649
+ <div className="bg-[var(--color-surface)] card p-4">
468
650
  <h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Activity</h3>
469
651
  <ResponsiveContainer width="100%" height={200}>
470
652
  <AreaChart data={chartData}>
@@ -506,6 +688,7 @@ export function TelemetryPage() {
506
688
  fill="var(--color-accent)"
507
689
  fillOpacity={0.15}
508
690
  strokeWidth={1.5}
691
+
509
692
  />
510
693
  <Area
511
694
  type="monotone"
@@ -515,6 +698,7 @@ export function TelemetryPage() {
515
698
  fill="var(--color-accent-hover)"
516
699
  fillOpacity={0.08}
517
700
  strokeWidth={1.5}
701
+
518
702
  />
519
703
  <Area
520
704
  type="monotone"
@@ -524,70 +708,97 @@ export function TelemetryPage() {
524
708
  fill="#ef4444"
525
709
  fillOpacity={0.1}
526
710
  strokeWidth={1.5}
711
+
527
712
  />
528
713
  </AreaChart>
529
714
  </ResponsiveContainer>
530
715
  </div>
531
716
 
532
- {/* Token Usage Chart */}
533
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
534
- <h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Token Usage</h3>
535
- <ResponsiveContainer width="100%" height={200}>
536
- <BarChart data={chartData}>
537
- <CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
538
- <XAxis
539
- dataKey="date"
540
- stroke="var(--color-border-light)"
541
- tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
542
- tickFormatter={(v) => {
543
- if (!useDaily && v.includes(" ")) {
544
- return v.split(" ")[1];
545
- }
546
- const d = new Date(v + "T00:00:00");
547
- return `${d.getMonth() + 1}/${d.getDate()}`;
548
- }}
549
- />
550
- <YAxis
551
- stroke="var(--color-border-light)"
552
- tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
553
- tickFormatter={(v) => {
554
- if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
555
- if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
556
- return v;
557
- }}
558
- />
559
- <Tooltip
560
- contentStyle={{
561
- backgroundColor: "var(--color-surface)",
562
- border: "1px solid var(--color-border-light)",
563
- borderRadius: "8px",
564
- fontSize: 12,
565
- }}
566
- labelStyle={{ color: "var(--color-text-secondary)" }}
567
- cursor={{ fill: "rgba(255,255,255,0.03)" }}
568
- labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
569
- formatter={(value: number) => [value.toLocaleString(), undefined]}
570
- />
571
- <Legend
572
- wrapperStyle={{ fontSize: 11 }}
573
- iconType="circle"
574
- iconSize={8}
575
- />
576
- <Bar
577
- dataKey="input_tokens"
578
- name="Input Tokens"
579
- fill="var(--color-accent)"
580
- radius={[2, 2, 0, 0]}
581
- />
582
- <Bar
583
- dataKey="output_tokens"
584
- name="Output Tokens"
585
- fill="var(--color-accent-hover)"
586
- radius={[2, 2, 0, 0]}
587
- />
588
- </BarChart>
589
- </ResponsiveContainer>
590
- </div>
717
+ {/* Token Usage Chart - stacked bars: Input (cache read / cache write / regular) and Output (reasoning / regular) */}
718
+ {(() => {
719
+ // Compute stacked segments for each bucket
720
+ const stackedData = chartData.map(d => {
721
+ const cacheRead = d.cache_read_tokens || 0;
722
+ const cacheWrite = d.cache_creation_tokens || 0;
723
+ const regularInput = Math.max(0, d.input_tokens - cacheRead - cacheWrite);
724
+ const reasoning = d.reasoning_tokens || 0;
725
+ const regularOutput = Math.max(0, d.output_tokens - reasoning);
726
+ return {
727
+ date: d.date,
728
+ // Input stack (bottom to top: cache read, cache write, regular)
729
+ input_cache_read: cacheRead,
730
+ input_cache_write: cacheWrite,
731
+ input_regular: regularInput,
732
+ // Output stack (bottom to top: reasoning, regular)
733
+ output_reasoning: reasoning,
734
+ output_regular: regularOutput,
735
+ };
736
+ });
737
+ const hasCache = stackedData.some(d => d.input_cache_read > 0 || d.input_cache_write > 0);
738
+ const hasReasoning = stackedData.some(d => d.output_reasoning > 0);
739
+
740
+ return (
741
+ <div className="bg-[var(--color-surface)] card p-4">
742
+ <h3 className="text-sm font-medium text-[var(--color-text-secondary)] mb-4">{chartLabel} Token Usage</h3>
743
+ <ResponsiveContainer width="100%" height={200}>
744
+ <BarChart data={stackedData}>
745
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
746
+ <XAxis
747
+ dataKey="date"
748
+ stroke="var(--color-border-light)"
749
+ tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
750
+ tickFormatter={(v) => {
751
+ if (!useDaily && v.includes(" ")) {
752
+ return v.split(" ")[1];
753
+ }
754
+ const d = new Date(v + "T00:00:00");
755
+ return `${d.getMonth() + 1}/${d.getDate()}`;
756
+ }}
757
+ />
758
+ <YAxis
759
+ stroke="var(--color-border-light)"
760
+ tick={{ fill: "var(--color-text-muted)", fontSize: 11 }}
761
+ tickFormatter={(v) => {
762
+ if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
763
+ if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
764
+ return v;
765
+ }}
766
+ />
767
+ <Tooltip
768
+ contentStyle={{
769
+ backgroundColor: "var(--color-surface)",
770
+ border: "1px solid var(--color-border-light)",
771
+ borderRadius: "8px",
772
+ fontSize: 12,
773
+ }}
774
+ labelStyle={{ color: "var(--color-text-secondary)" }}
775
+ cursor={{ fill: "rgba(255,255,255,0.03)" }}
776
+ labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
777
+ formatter={(value: number, name: string) => [value.toLocaleString(), name]}
778
+ />
779
+ <Legend
780
+ wrapperStyle={{ fontSize: 11 }}
781
+ iconType="circle"
782
+ iconSize={8}
783
+ />
784
+ {/* Input stack */}
785
+ {hasCache && (
786
+ <Bar dataKey="input_cache_read" name="Input (Cache Read)" stackId="input" fill="#fdba74" />
787
+ )}
788
+ {hasCache && (
789
+ <Bar dataKey="input_cache_write" name="Input (Cache Write)" stackId="input" fill="#fb923c" />
790
+ )}
791
+ <Bar dataKey="input_regular" name={hasCache ? "Input (Regular)" : "Input Tokens"} stackId="input" fill="#f97316" radius={[2, 2, 0, 0]} />
792
+ {/* Output stack */}
793
+ {hasReasoning && (
794
+ <Bar dataKey="output_reasoning" name="Output (Reasoning)" stackId="output" fill="#c4b5fd" />
795
+ )}
796
+ <Bar dataKey="output_regular" name={hasReasoning ? "Output (Regular)" : "Output Tokens"} stackId="output" fill="#a78bfa" radius={[2, 2, 0, 0]} />
797
+ </BarChart>
798
+ </ResponsiveContainer>
799
+ </div>
800
+ );
801
+ })()}
591
802
  </div>
592
803
  );
593
804
  })()}
@@ -595,6 +806,8 @@ export function TelemetryPage() {
595
806
  {/* Usage by Agent */}
596
807
  {usage.length > 0 && (() => {
597
808
  const maxCost = Math.max(...sortedUsage.map(u => u.cost), 0.0001);
809
+ const hasCacheTokens = sortedUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
810
+ const hasReasoningTokens = sortedUsage.some(u => (u.reasoning_tokens || 0) > 0);
598
811
  const SortHeader = ({ label, field, align = "right" }: { label: string; field: SortKey; align?: string }) => (
599
812
  <th
600
813
  className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
@@ -615,7 +828,7 @@ export function TelemetryPage() {
615
828
  return (
616
829
  <div className="mb-6">
617
830
  <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">
831
+ <div className="bg-[var(--color-surface)] card overflow-hidden">
619
832
  <table className="w-full text-sm">
620
833
  <thead>
621
834
  <tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
@@ -624,6 +837,9 @@ export function TelemetryPage() {
624
837
  <SortHeader label="Tool Calls" field="tool_calls" />
625
838
  <SortHeader label="Input Tokens" field="input_tokens" />
626
839
  <SortHeader label="Output Tokens" field="output_tokens" />
840
+ {hasCacheTokens && <SortHeader label="Cache Write" field="cache_creation_tokens" />}
841
+ {hasCacheTokens && <SortHeader label="Cache Read" field="cache_read_tokens" />}
842
+ {hasReasoningTokens && <SortHeader label="Reasoning" field="reasoning_tokens" />}
627
843
  <SortHeader label="Errors" field="errors" />
628
844
  {costTrackingEnabled && <SortHeader label="Est. Cost" field="cost" />}
629
845
  </tr>
@@ -636,6 +852,15 @@ export function TelemetryPage() {
636
852
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
637
853
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
638
854
  <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
855
+ {hasCacheTokens && (
856
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
857
+ )}
858
+ {hasCacheTokens && (
859
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
860
+ )}
861
+ {hasReasoningTokens && (
862
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
863
+ )}
639
864
  <td className="p-3 text-right">
640
865
  {u.errors > 0 ? (
641
866
  <span className="text-red-400">{u.errors}</span>
@@ -665,6 +890,101 @@ export function TelemetryPage() {
665
890
  );
666
891
  })()}
667
892
 
893
+ {/* Usage by Project */}
894
+ {projectsEnabled && currentProjectId === null && sortedProjectUsage.length > 0 && (() => {
895
+ const maxCost = Math.max(...sortedProjectUsage.map(u => u.cost), 0.0001);
896
+ const hasProjCacheTokens = sortedProjectUsage.some(u => (u.cache_creation_tokens || 0) > 0 || (u.cache_read_tokens || 0) > 0);
897
+ const hasProjReasoningTokens = sortedProjectUsage.some(u => (u.reasoning_tokens || 0) > 0);
898
+ const PSortHeader = ({ label, field, align = "right" }: { label: string; field: ProjectSortKey; align?: string }) => (
899
+ <th
900
+ className={`${align === "left" ? "text-left" : "text-right"} p-3 cursor-pointer hover:text-[var(--color-text-secondary)] select-none transition-colors`}
901
+ onClick={() => handleProjectSort(field)}
902
+ >
903
+ <span className="inline-flex items-center gap-1">
904
+ {align === "right" && projectSortKey === field && (
905
+ <span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
906
+ )}
907
+ {label}
908
+ {align === "left" && projectSortKey === field && (
909
+ <span className="text-orange-400">{projectSortDir === "asc" ? "\u25b2" : "\u25bc"}</span>
910
+ )}
911
+ </span>
912
+ </th>
913
+ );
914
+
915
+ return (
916
+ <div className="mb-6">
917
+ <h2 className="text-lg font-medium mb-3">Usage by Project</h2>
918
+ <div className="bg-[var(--color-surface)] card overflow-hidden">
919
+ <table className="w-full text-sm">
920
+ <thead>
921
+ <tr className="border-b border-[var(--color-border)] text-[var(--color-text-muted)]">
922
+ <PSortHeader label="Project" field="project" align="left" />
923
+ <PSortHeader label="LLM Calls" field="llm_calls" />
924
+ <PSortHeader label="Tool Calls" field="tool_calls" />
925
+ <PSortHeader label="Input Tokens" field="input_tokens" />
926
+ <PSortHeader label="Output Tokens" field="output_tokens" />
927
+ {hasProjCacheTokens && <PSortHeader label="Cache Write" field="cache_creation_tokens" />}
928
+ {hasProjCacheTokens && <PSortHeader label="Cache Read" field="cache_read_tokens" />}
929
+ {hasProjReasoningTokens && <PSortHeader label="Reasoning" field="reasoning_tokens" />}
930
+ <PSortHeader label="Errors" field="errors" />
931
+ {costTrackingEnabled && <PSortHeader label="Est. Cost" field="cost" />}
932
+ </tr>
933
+ </thead>
934
+ <tbody>
935
+ {sortedProjectUsage.map((u) => {
936
+ const color = getProjectColor(u.project_id);
937
+ return (
938
+ <tr key={u.project_id || "_unassigned"} className="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg)]">
939
+ <td className="p-3 font-medium">
940
+ <span className="inline-flex items-center gap-2">
941
+ {color && <span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />}
942
+ {getProjectName(u.project_id)}
943
+ </span>
944
+ </td>
945
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.llm_calls)}</td>
946
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.tool_calls)}</td>
947
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.input_tokens)}</td>
948
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.output_tokens)}</td>
949
+ {hasProjCacheTokens && (
950
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_creation_tokens || 0)}</td>
951
+ )}
952
+ {hasProjCacheTokens && (
953
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.cache_read_tokens || 0)}</td>
954
+ )}
955
+ {hasProjReasoningTokens && (
956
+ <td className="p-3 text-right text-[var(--color-text-secondary)]">{formatNumber(u.reasoning_tokens || 0)}</td>
957
+ )}
958
+ <td className="p-3 text-right">
959
+ {u.errors > 0 ? (
960
+ <span className="text-red-400">{u.errors}</span>
961
+ ) : (
962
+ <span className="text-[var(--color-text-faint)]">0</span>
963
+ )}
964
+ </td>
965
+ {costTrackingEnabled && (
966
+ <td className="p-3 text-right">
967
+ <div className="flex items-center justify-end gap-2">
968
+ <div className="w-16 h-1.5 bg-[var(--color-surface-raised)] rounded-full overflow-hidden">
969
+ <div
970
+ className="h-full bg-orange-500 rounded-full"
971
+ style={{ width: `${(u.cost / maxCost) * 100}%` }}
972
+ />
973
+ </div>
974
+ <span className="text-[var(--color-text-secondary)] min-w-[60px] text-right">${u.cost.toFixed(4)}</span>
975
+ </div>
976
+ </td>
977
+ )}
978
+ </tr>
979
+ );
980
+ })}
981
+ </tbody>
982
+ </table>
983
+ </div>
984
+ </div>
985
+ );
986
+ })()}
987
+
668
988
  {/* Filters */}
669
989
  <div className="flex flex-wrap items-center gap-3 mb-4">
670
990
  <div className="w-44">
@@ -714,7 +1034,7 @@ export function TelemetryPage() {
714
1034
  </div>
715
1035
 
716
1036
  {/* Events List */}
717
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg">
1037
+ <div className="bg-[var(--color-surface)] card">
718
1038
  <div className="p-3 border-b border-[var(--color-border)] flex items-center justify-between">
719
1039
  <h2 className="font-medium">Recent Events</h2>
720
1040
  {realtimeEvents.length > 0 && (
@@ -728,7 +1048,7 @@ export function TelemetryPage() {
728
1048
  <div className="p-8 text-center text-[var(--color-text-muted)]">Loading...</div>
729
1049
  ) : allEvents.length === 0 ? (
730
1050
  <div className="p-8 text-center text-[var(--color-text-muted)]">
731
- No telemetry events yet. Events will appear here in real-time once agents start sending data.
1051
+ No events yet. Events will appear here in real-time once agents start sending data.
732
1052
  </div>
733
1053
  ) : (
734
1054
  <div className="divide-y divide-[var(--color-border)]">
@@ -791,7 +1111,7 @@ export function TelemetryPage() {
791
1111
 
792
1112
  function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
793
1113
  return (
794
- <div className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-lg p-4">
1114
+ <div className="bg-[var(--color-surface)] card p-4 flex-1 min-w-[120px]">
795
1115
  <div className="text-[var(--color-text-muted)] text-xs mb-1">{label}</div>
796
1116
  <div className={`text-2xl font-semibold ${color === "red" ? "text-red-400" : color === "orange" ? "text-orange-400" : ""}`}>
797
1117
  {value}