apteva 0.2.5 → 0.2.7

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.
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
2
2
  import { CheckIcon } from "../common/Icons";
3
3
  import type { Provider } from "../../types";
4
4
 
5
- type SettingsTab = "providers" | "updates";
5
+ type SettingsTab = "providers" | "updates" | "data";
6
6
 
7
7
  export function SettingsPage() {
8
8
  const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
@@ -23,6 +23,11 @@ export function SettingsPage() {
23
23
  active={activeTab === "updates"}
24
24
  onClick={() => setActiveTab("updates")}
25
25
  />
26
+ <SettingsNavItem
27
+ label="Data"
28
+ active={activeTab === "data"}
29
+ onClick={() => setActiveTab("data")}
30
+ />
26
31
  </nav>
27
32
  </div>
28
33
 
@@ -30,6 +35,7 @@ export function SettingsPage() {
30
35
  <div className="flex-1 overflow-auto p-6">
31
36
  {activeTab === "providers" && <ProvidersSettings />}
32
37
  {activeTab === "updates" && <UpdatesSettings />}
38
+ {activeTab === "data" && <DataSettings />}
33
39
  </div>
34
40
  </div>
35
41
  );
@@ -628,3 +634,84 @@ function IntegrationKeyCard({
628
634
  </div>
629
635
  );
630
636
  }
637
+
638
+ function DataSettings() {
639
+ const [clearing, setClearing] = useState(false);
640
+ const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
641
+ const [eventCount, setEventCount] = useState<number | null>(null);
642
+
643
+ const fetchStats = async () => {
644
+ try {
645
+ const res = await fetch("/api/telemetry/stats");
646
+ const data = await res.json();
647
+ setEventCount(data.stats?.total_events || 0);
648
+ } catch {
649
+ setEventCount(null);
650
+ }
651
+ };
652
+
653
+ useEffect(() => {
654
+ fetchStats();
655
+ }, []);
656
+
657
+ const clearTelemetry = async () => {
658
+ if (!confirm("Are you sure you want to delete all telemetry data? This cannot be undone.")) {
659
+ return;
660
+ }
661
+
662
+ setClearing(true);
663
+ setMessage(null);
664
+
665
+ try {
666
+ const res = await fetch("/api/telemetry/clear", { method: "POST" });
667
+ const data = await res.json();
668
+
669
+ if (res.ok) {
670
+ setMessage({ type: "success", text: `Cleared ${data.deleted || 0} telemetry events.` });
671
+ setEventCount(0);
672
+ } else {
673
+ setMessage({ type: "error", text: data.error || "Failed to clear telemetry" });
674
+ }
675
+ } catch {
676
+ setMessage({ type: "error", text: "Failed to clear telemetry" });
677
+ }
678
+
679
+ setClearing(false);
680
+ };
681
+
682
+ return (
683
+ <div className="max-w-2xl">
684
+ <div className="mb-6">
685
+ <h1 className="text-2xl font-semibold mb-1">Data Management</h1>
686
+ <p className="text-[#666]">Manage stored data and telemetry.</p>
687
+ </div>
688
+
689
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
690
+ <h3 className="font-medium mb-2">Telemetry Data</h3>
691
+ <p className="text-sm text-[#666] mb-4">
692
+ {eventCount !== null
693
+ ? `${eventCount.toLocaleString()} events stored`
694
+ : "Loading..."}
695
+ </p>
696
+
697
+ {message && (
698
+ <div className={`mb-4 p-3 rounded text-sm ${
699
+ message.type === "success"
700
+ ? "bg-green-500/10 text-green-400 border border-green-500/30"
701
+ : "bg-red-500/10 text-red-400 border border-red-500/30"
702
+ }`}>
703
+ {message.text}
704
+ </div>
705
+ )}
706
+
707
+ <button
708
+ onClick={clearTelemetry}
709
+ disabled={clearing || eventCount === 0}
710
+ className="px-4 py-2 bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed rounded text-sm font-medium transition"
711
+ >
712
+ {clearing ? "Clearing..." : "Clear All Telemetry"}
713
+ </button>
714
+ </div>
715
+ </div>
716
+ );
717
+ }
@@ -1,5 +1,6 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React, { useState, useEffect, useMemo, useRef } from "react";
2
2
  import { Select } from "../common/Select";
3
+ import { useTelemetryContext, type TelemetryEvent } from "../../context";
3
4
 
4
5
  interface TelemetryStats {
5
6
  total_events: number;
@@ -10,20 +11,6 @@ interface TelemetryStats {
10
11
  total_output_tokens: number;
11
12
  }
12
13
 
13
- interface TelemetryEvent {
14
- id: string;
15
- agent_id: string;
16
- timestamp: string;
17
- category: string;
18
- type: string;
19
- level: string;
20
- trace_id: string | null;
21
- thread_id: string | null;
22
- data: Record<string, unknown> | null;
23
- duration_ms: number | null;
24
- error: string | null;
25
- }
26
-
27
14
  interface UsageByAgent {
28
15
  agent_id: string;
29
16
  input_tokens: number;
@@ -33,10 +20,34 @@ interface UsageByAgent {
33
20
  errors: number;
34
21
  }
35
22
 
23
+ // Helper to extract stats from a single event
24
+ function extractEventStats(event: TelemetryEvent): {
25
+ llm_calls: number;
26
+ tool_calls: number;
27
+ errors: number;
28
+ input_tokens: number;
29
+ output_tokens: number;
30
+ } {
31
+ const isLlm = event.category === "LLM";
32
+ const isTool = event.category === "TOOL";
33
+ const isError = event.level === "error";
34
+ const inputTokens = (event.data?.input_tokens as number) || 0;
35
+ const outputTokens = (event.data?.output_tokens as number) || 0;
36
+
37
+ return {
38
+ llm_calls: isLlm ? 1 : 0,
39
+ tool_calls: isTool ? 1 : 0,
40
+ errors: isError ? 1 : 0,
41
+ input_tokens: inputTokens,
42
+ output_tokens: outputTokens,
43
+ };
44
+ }
45
+
36
46
  export function TelemetryPage() {
37
- const [stats, setStats] = useState<TelemetryStats | null>(null);
38
- const [events, setEvents] = useState<TelemetryEvent[]>([]);
39
- const [usage, setUsage] = useState<UsageByAgent[]>([]);
47
+ const { events: realtimeEvents } = useTelemetryContext();
48
+ const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
49
+ const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
50
+ const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
40
51
  const [loading, setLoading] = useState(true);
41
52
  const [filter, setFilter] = useState({
42
53
  category: "",
@@ -46,6 +57,9 @@ export function TelemetryPage() {
46
57
  const [agents, setAgents] = useState<Array<{ id: string; name: string }>>([]);
47
58
  const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
48
59
 
60
+ // Track IDs that were in the fetched stats to avoid double-counting
61
+ const countedEventIdsRef = useRef<Set<string>>(new Set());
62
+
49
63
  // Fetch agents for dropdown
50
64
  useEffect(() => {
51
65
  const fetchAgents = async () => {
@@ -60,16 +74,16 @@ export function TelemetryPage() {
60
74
  fetchAgents();
61
75
  }, []);
62
76
 
63
- // Fetch telemetry data
77
+ // Fetch stats and historical data (less frequently now since we have real-time)
64
78
  const fetchData = async () => {
65
79
  setLoading(true);
66
80
  try {
67
81
  // Fetch stats
68
82
  const statsRes = await fetch("/api/telemetry/stats");
69
83
  const statsData = await statsRes.json();
70
- setStats(statsData.stats);
84
+ setFetchedStats(statsData.stats);
71
85
 
72
- // Fetch events with filters
86
+ // Fetch historical events with filters
73
87
  const params = new URLSearchParams();
74
88
  if (filter.category) params.set("category", filter.category);
75
89
  if (filter.level) params.set("level", filter.level);
@@ -78,12 +92,16 @@ export function TelemetryPage() {
78
92
 
79
93
  const eventsRes = await fetch(`/api/telemetry/events?${params}`);
80
94
  const eventsData = await eventsRes.json();
81
- setEvents(eventsData.events || []);
95
+ const events = eventsData.events || [];
96
+ setHistoricalEvents(events);
97
+
98
+ // Mark all fetched event IDs as counted (stats already include them)
99
+ countedEventIdsRef.current = new Set(events.map((e: TelemetryEvent) => e.id));
82
100
 
83
101
  // Fetch usage by agent
84
102
  const usageRes = await fetch("/api/telemetry/usage?group_by=agent");
85
103
  const usageData = await usageRes.json();
86
- setUsage(usageData.usage || []);
104
+ setFetchedUsage(usageData.usage || []);
87
105
  } catch (e) {
88
106
  console.error("Failed to fetch telemetry:", e);
89
107
  }
@@ -92,11 +110,110 @@ export function TelemetryPage() {
92
110
 
93
111
  useEffect(() => {
94
112
  fetchData();
95
- // Auto-refresh every 30 seconds
96
- const interval = setInterval(fetchData, 30000);
113
+ // Refresh stats every 60 seconds (events come in real-time)
114
+ const interval = setInterval(fetchData, 60000);
97
115
  return () => clearInterval(interval);
98
116
  }, [filter]);
99
117
 
118
+ // Compute real-time stats from new events (not already counted in fetched stats)
119
+ const stats = useMemo(() => {
120
+ if (!fetchedStats) return null;
121
+
122
+ // Calculate deltas from real-time events not in fetched data
123
+ let deltaEvents = 0;
124
+ let deltaLlmCalls = 0;
125
+ let deltaToolCalls = 0;
126
+ let deltaErrors = 0;
127
+ let deltaInputTokens = 0;
128
+ let deltaOutputTokens = 0;
129
+
130
+ for (const event of realtimeEvents) {
131
+ if (!countedEventIdsRef.current.has(event.id)) {
132
+ deltaEvents++;
133
+ const eventStats = extractEventStats(event);
134
+ deltaLlmCalls += eventStats.llm_calls;
135
+ deltaToolCalls += eventStats.tool_calls;
136
+ deltaErrors += eventStats.errors;
137
+ deltaInputTokens += eventStats.input_tokens;
138
+ deltaOutputTokens += eventStats.output_tokens;
139
+ }
140
+ }
141
+
142
+ return {
143
+ total_events: fetchedStats.total_events + deltaEvents,
144
+ total_llm_calls: fetchedStats.total_llm_calls + deltaLlmCalls,
145
+ total_tool_calls: fetchedStats.total_tool_calls + deltaToolCalls,
146
+ total_errors: fetchedStats.total_errors + deltaErrors,
147
+ total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
148
+ total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
149
+ };
150
+ }, [fetchedStats, realtimeEvents]);
151
+
152
+ // Compute real-time usage by agent
153
+ const usage = useMemo(() => {
154
+ // Start with a copy of fetched usage as a map
155
+ const usageMap = new Map<string, UsageByAgent>();
156
+ for (const u of fetchedUsage) {
157
+ usageMap.set(u.agent_id, { ...u });
158
+ }
159
+
160
+ // Add deltas from real-time events
161
+ for (const event of realtimeEvents) {
162
+ if (!countedEventIdsRef.current.has(event.id)) {
163
+ const eventStats = extractEventStats(event);
164
+ const existing = usageMap.get(event.agent_id);
165
+ if (existing) {
166
+ existing.llm_calls += eventStats.llm_calls;
167
+ existing.tool_calls += eventStats.tool_calls;
168
+ existing.errors += eventStats.errors;
169
+ existing.input_tokens += eventStats.input_tokens;
170
+ existing.output_tokens += eventStats.output_tokens;
171
+ } else {
172
+ usageMap.set(event.agent_id, {
173
+ agent_id: event.agent_id,
174
+ llm_calls: eventStats.llm_calls,
175
+ tool_calls: eventStats.tool_calls,
176
+ errors: eventStats.errors,
177
+ input_tokens: eventStats.input_tokens,
178
+ output_tokens: eventStats.output_tokens,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ return Array.from(usageMap.values());
185
+ }, [fetchedUsage, realtimeEvents]);
186
+
187
+ // Merge real-time events with historical, filtering and deduping
188
+ const allEvents = React.useMemo(() => {
189
+ // Apply filters to real-time events
190
+ let filtered = realtimeEvents;
191
+ if (filter.agent_id) {
192
+ filtered = filtered.filter(e => e.agent_id === filter.agent_id);
193
+ }
194
+ if (filter.category) {
195
+ filtered = filtered.filter(e => e.category === filter.category);
196
+ }
197
+ if (filter.level) {
198
+ filtered = filtered.filter(e => e.level === filter.level);
199
+ }
200
+
201
+ // Merge with historical, dedupe by ID
202
+ const seen = new Set(filtered.map(e => e.id));
203
+ const merged = [...filtered];
204
+ for (const evt of historicalEvents) {
205
+ if (!seen.has(evt.id)) {
206
+ merged.push(evt);
207
+ seen.add(evt.id);
208
+ }
209
+ }
210
+
211
+ // Sort by timestamp descending
212
+ merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
213
+
214
+ return merged.slice(0, 100);
215
+ }, [realtimeEvents, historicalEvents, filter]);
216
+
100
217
  const getAgentName = (agentId: string) => {
101
218
  const agent = agents.find(a => a.id === agentId);
102
219
  return agent?.name || agentId;
@@ -249,53 +366,67 @@ export function TelemetryPage() {
249
366
 
250
367
  {/* Events List */}
251
368
  <div className="bg-[#111] border border-[#1a1a1a] rounded-lg">
252
- <div className="p-3 border-b border-[#1a1a1a]">
369
+ <div className="p-3 border-b border-[#1a1a1a] flex items-center justify-between">
253
370
  <h2 className="font-medium">Recent Events</h2>
371
+ {realtimeEvents.length > 0 && (
372
+ <span className="text-xs text-[#666]">
373
+ {realtimeEvents.length} new
374
+ </span>
375
+ )}
254
376
  </div>
255
377
 
256
- {loading && events.length === 0 ? (
378
+ {loading && allEvents.length === 0 ? (
257
379
  <div className="p-8 text-center text-[#666]">Loading...</div>
258
- ) : events.length === 0 ? (
380
+ ) : allEvents.length === 0 ? (
259
381
  <div className="p-8 text-center text-[#666]">
260
- No telemetry events yet. Events will appear here once agents start sending data.
382
+ No telemetry events yet. Events will appear here in real-time once agents start sending data.
261
383
  </div>
262
384
  ) : (
263
385
  <div className="divide-y divide-[#1a1a1a]">
264
- {events.map((event) => (
265
- <div
266
- key={event.id}
267
- className="p-3 hover:bg-[#0a0a0a] transition cursor-pointer"
268
- onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
269
- >
270
- <div className="flex items-start gap-3">
271
- <span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
272
- {event.category}
273
- </span>
274
- <div className="flex-1 min-w-0">
275
- <div className="flex items-center gap-2">
276
- <span className="font-medium text-sm">{event.type}</span>
277
- <span className={`text-xs ${levelColors[event.level] || "text-[#666]"}`}>
278
- {event.level}
279
- </span>
280
- {event.duration_ms && (
281
- <span className="text-xs text-[#555]">{event.duration_ms}ms</span>
386
+ {allEvents.map((event) => {
387
+ // Only mark as new if event arrived in last 10 seconds
388
+ const eventTime = new Date(event.timestamp).getTime();
389
+ const isNew = Date.now() - eventTime < 10000;
390
+
391
+ return (
392
+ <div
393
+ key={event.id}
394
+ className="p-3 hover:bg-[#0a0a0a] transition cursor-pointer"
395
+ onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
396
+ >
397
+ <div className="flex items-start gap-3">
398
+ <span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
399
+ {event.category}
400
+ </span>
401
+ <div className="flex-1 min-w-0">
402
+ <div className="flex items-center gap-2">
403
+ <span className="font-medium text-sm">{event.type}</span>
404
+ <span className={`text-xs ${levelColors[event.level] || "text-[#666]"}`}>
405
+ {event.level}
406
+ </span>
407
+ {event.duration_ms && (
408
+ <span className="text-xs text-[#555]">{event.duration_ms}ms</span>
409
+ )}
410
+ {isNew && (
411
+ <span className="w-1.5 h-1.5 rounded-full bg-green-400" />
412
+ )}
413
+ </div>
414
+ <div className="text-xs text-[#555] mt-1">
415
+ {getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
416
+ </div>
417
+ {event.error && (
418
+ <div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
419
+ )}
420
+ {expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
421
+ <pre className="text-xs text-[#666] mt-2 p-2 bg-[#0a0a0a] rounded overflow-x-auto">
422
+ {JSON.stringify(event.data, null, 2)}
423
+ </pre>
282
424
  )}
283
425
  </div>
284
- <div className="text-xs text-[#555] mt-1">
285
- {getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
286
- </div>
287
- {event.error && (
288
- <div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
289
- )}
290
- {expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
291
- <pre className="text-xs text-[#666] mt-2 p-2 bg-[#0a0a0a] rounded overflow-x-auto">
292
- {JSON.stringify(event.data, null, 2)}
293
- </pre>
294
- )}
295
426
  </div>
296
427
  </div>
297
- </div>
298
- ))}
428
+ );
429
+ })}
299
430
  </div>
300
431
  )}
301
432
  </div>
@@ -0,0 +1,224 @@
1
+ import React, { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from "react";
2
+
3
+ export interface TelemetryEvent {
4
+ id: string;
5
+ agent_id: string;
6
+ timestamp: string;
7
+ category: string;
8
+ type: string;
9
+ level: string;
10
+ trace_id?: string;
11
+ thread_id?: string;
12
+ data?: Record<string, unknown>;
13
+ duration_ms?: number;
14
+ error?: string;
15
+ }
16
+
17
+ interface TelemetryContextValue {
18
+ connected: boolean;
19
+ events: TelemetryEvent[];
20
+ lastActivityByAgent: Record<string, { timestamp: string; category: string; type: string }>;
21
+ activeAgents: Record<string, { type: string; expiresAt: number }>;
22
+ clearEvents: () => void;
23
+ }
24
+
25
+ const TelemetryContext = createContext<TelemetryContextValue | null>(null);
26
+
27
+ const MAX_EVENTS = 200; // Keep last 200 events in memory
28
+
29
+ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
30
+ const [connected, setConnected] = useState(false);
31
+ const [events, setEvents] = useState<TelemetryEvent[]>([]);
32
+ const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
33
+ const [activeAgents, setActiveAgents] = useState<Record<string, { type: string; expiresAt: number }>>({});
34
+ const eventSourceRef = useRef<EventSource | null>(null);
35
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
36
+
37
+ // Clean up expired active states
38
+ useEffect(() => {
39
+ const interval = setInterval(() => {
40
+ const now = Date.now();
41
+ setActiveAgents(prev => {
42
+ const updated: Record<string, { type: string; expiresAt: number }> = {};
43
+ for (const [agentId, state] of Object.entries(prev)) {
44
+ if (state.expiresAt > now) {
45
+ updated[agentId] = state;
46
+ }
47
+ }
48
+ return updated;
49
+ });
50
+ }, 500);
51
+ return () => clearInterval(interval);
52
+ }, []);
53
+
54
+ const connect = useCallback(() => {
55
+ if (eventSourceRef.current) {
56
+ eventSourceRef.current.close();
57
+ eventSourceRef.current = null;
58
+ }
59
+
60
+ try {
61
+ const es = new EventSource("/api/telemetry/stream");
62
+ eventSourceRef.current = es;
63
+
64
+ es.onopen = () => {
65
+ setConnected(true);
66
+ };
67
+
68
+ es.onmessage = (event) => {
69
+ // Ignore keepalive pings (comments starting with :)
70
+ if (!event.data || event.data.trim() === "") return;
71
+
72
+ try {
73
+ const data = JSON.parse(event.data);
74
+
75
+ // Handle connection message
76
+ if (data.connected) {
77
+ setConnected(true);
78
+ return;
79
+ }
80
+
81
+ // Handle array of events
82
+ if (Array.isArray(data)) {
83
+ setEvents(prev => {
84
+ const combined = [...data, ...prev];
85
+ return combined.slice(0, MAX_EVENTS);
86
+ });
87
+
88
+ // Update last activity per agent
89
+ setLastActivityByAgent(prev => {
90
+ const updated = { ...prev };
91
+ for (const evt of data) {
92
+ const existing = updated[evt.agent_id];
93
+ if (!existing || new Date(evt.timestamp) > new Date(existing.timestamp)) {
94
+ updated[evt.agent_id] = {
95
+ timestamp: evt.timestamp,
96
+ category: evt.category,
97
+ type: evt.type,
98
+ };
99
+ }
100
+ }
101
+ return updated;
102
+ });
103
+
104
+ // Set agents as active for 3 seconds (tracked in context, not component)
105
+ setActiveAgents(prev => {
106
+ const updated = { ...prev };
107
+ const expiresAt = Date.now() + 3000;
108
+ for (const evt of data) {
109
+ updated[evt.agent_id] = { type: evt.type, expiresAt };
110
+ }
111
+ return updated;
112
+ });
113
+ }
114
+ } catch {
115
+ // Ignore parse errors (likely keepalive or empty message)
116
+ }
117
+ };
118
+
119
+ es.onerror = () => {
120
+ setConnected(false);
121
+ es.close();
122
+ eventSourceRef.current = null;
123
+
124
+ // Reconnect after 2 seconds
125
+ if (reconnectTimeoutRef.current) {
126
+ clearTimeout(reconnectTimeoutRef.current);
127
+ }
128
+ reconnectTimeoutRef.current = setTimeout(connect, 2000);
129
+ };
130
+ } catch {
131
+ // Failed to create EventSource, retry
132
+ setConnected(false);
133
+ if (reconnectTimeoutRef.current) {
134
+ clearTimeout(reconnectTimeoutRef.current);
135
+ }
136
+ reconnectTimeoutRef.current = setTimeout(connect, 2000);
137
+ }
138
+ }, []);
139
+
140
+ useEffect(() => {
141
+ connect();
142
+
143
+ return () => {
144
+ if (eventSourceRef.current) {
145
+ eventSourceRef.current.close();
146
+ }
147
+ if (reconnectTimeoutRef.current) {
148
+ clearTimeout(reconnectTimeoutRef.current);
149
+ }
150
+ };
151
+ }, [connect]);
152
+
153
+ const clearEvents = useCallback(() => {
154
+ setEvents([]);
155
+ }, []);
156
+
157
+ return (
158
+ <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, clearEvents }}>
159
+ {children}
160
+ </TelemetryContext.Provider>
161
+ );
162
+ }
163
+
164
+ // Hook to access all telemetry
165
+ export function useTelemetryContext() {
166
+ const context = useContext(TelemetryContext);
167
+ if (!context) {
168
+ throw new Error("useTelemetryContext must be used within TelemetryProvider");
169
+ }
170
+ return context;
171
+ }
172
+
173
+ // Hook to filter telemetry for a specific agent or category
174
+ export function useTelemetry(filter?: {
175
+ agent_id?: string;
176
+ category?: string;
177
+ limit?: number;
178
+ }) {
179
+ const { connected, events, lastActivityByAgent } = useTelemetryContext();
180
+
181
+ const filteredEvents = React.useMemo(() => {
182
+ let result = events;
183
+
184
+ if (filter?.agent_id) {
185
+ result = result.filter(e => e.agent_id === filter.agent_id);
186
+ }
187
+ if (filter?.category) {
188
+ result = result.filter(e => e.category === filter.category);
189
+ }
190
+ if (filter?.limit) {
191
+ result = result.slice(0, filter.limit);
192
+ }
193
+
194
+ return result;
195
+ }, [events, filter?.agent_id, filter?.category, filter?.limit]);
196
+
197
+ const lastActivity = filter?.agent_id ? lastActivityByAgent[filter.agent_id] : undefined;
198
+
199
+ // Check if agent is "active" (had activity in last 10 seconds)
200
+ const isActive = React.useMemo(() => {
201
+ if (!lastActivity) return false;
202
+ const activityTime = new Date(lastActivity.timestamp).getTime();
203
+ const now = Date.now();
204
+ return now - activityTime < 10000; // 10 seconds
205
+ }, [lastActivity]);
206
+
207
+ return {
208
+ connected,
209
+ events: filteredEvents,
210
+ lastActivity,
211
+ isActive,
212
+ };
213
+ }
214
+
215
+ // Hook for agent activity indicator - uses context-level tracking
216
+ export function useAgentActivity(agentId: string) {
217
+ const { activeAgents } = useTelemetryContext();
218
+ const activity = activeAgents[agentId];
219
+
220
+ return {
221
+ isActive: !!activity,
222
+ type: activity?.type,
223
+ };
224
+ }
@@ -0,0 +1,2 @@
1
+ export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity } from "./TelemetryContext";
2
+ export type { TelemetryEvent } from "./TelemetryContext";