apteva 0.2.3 → 0.2.6
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.
- package/dist/App.0mzj9cz9.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/binary.ts +271 -1
- package/src/crypto.ts +53 -0
- package/src/db.ts +492 -3
- package/src/mcp-client.ts +599 -0
- package/src/providers.ts +31 -0
- package/src/routes/api.ts +832 -64
- package/src/server.ts +169 -5
- package/src/web/App.tsx +44 -2
- package/src/web/components/agents/AgentCard.tsx +53 -9
- package/src/web/components/agents/AgentPanel.tsx +381 -0
- package/src/web/components/agents/AgentsView.tsx +27 -10
- package/src/web/components/agents/CreateAgentModal.tsx +7 -7
- package/src/web/components/agents/index.ts +1 -1
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +2 -2
- package/src/web/components/common/Select.tsx +1 -1
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +74 -25
- package/src/web/components/index.ts +5 -2
- package/src/web/components/layout/Sidebar.tsx +22 -2
- package/src/web/components/mcp/McpPage.tsx +1144 -0
- package/src/web/components/mcp/index.ts +1 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
- package/src/web/components/settings/SettingsPage.tsx +312 -82
- package/src/web/components/tasks/TasksPage.tsx +129 -0
- package/src/web/components/tasks/index.ts +1 -0
- package/src/web/components/telemetry/TelemetryPage.tsx +359 -0
- package/src/web/context/TelemetryContext.tsx +202 -0
- package/src/web/context/index.ts +2 -0
- package/src/web/hooks/useAgents.ts +23 -0
- package/src/web/styles.css +18 -0
- package/src/web/types.ts +75 -1
- package/dist/App.wfhmfhx7.js +0 -213
- package/src/web/components/agents/ChatPanel.tsx +0 -63
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Select } from "../common/Select";
|
|
3
|
+
import { useTelemetryContext, type TelemetryEvent } from "../../context";
|
|
4
|
+
|
|
5
|
+
interface TelemetryStats {
|
|
6
|
+
total_events: number;
|
|
7
|
+
total_llm_calls: number;
|
|
8
|
+
total_tool_calls: number;
|
|
9
|
+
total_errors: number;
|
|
10
|
+
total_input_tokens: number;
|
|
11
|
+
total_output_tokens: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UsageByAgent {
|
|
15
|
+
agent_id: string;
|
|
16
|
+
input_tokens: number;
|
|
17
|
+
output_tokens: number;
|
|
18
|
+
llm_calls: number;
|
|
19
|
+
tool_calls: number;
|
|
20
|
+
errors: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TelemetryPage() {
|
|
24
|
+
const { connected, events: realtimeEvents } = useTelemetryContext();
|
|
25
|
+
const [stats, setStats] = useState<TelemetryStats | null>(null);
|
|
26
|
+
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
27
|
+
const [usage, setUsage] = useState<UsageByAgent[]>([]);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [filter, setFilter] = useState({
|
|
30
|
+
category: "",
|
|
31
|
+
level: "",
|
|
32
|
+
agent_id: "",
|
|
33
|
+
});
|
|
34
|
+
const [agents, setAgents] = useState<Array<{ id: string; name: string }>>([]);
|
|
35
|
+
const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
// Fetch agents for dropdown
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const fetchAgents = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch("/api/agents");
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
setAgents(data.agents || []);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error("Failed to fetch agents:", e);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
fetchAgents();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Fetch stats and historical data (less frequently now since we have real-time)
|
|
52
|
+
const fetchData = async () => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
// Fetch stats
|
|
56
|
+
const statsRes = await fetch("/api/telemetry/stats");
|
|
57
|
+
const statsData = await statsRes.json();
|
|
58
|
+
setStats(statsData.stats);
|
|
59
|
+
|
|
60
|
+
// Fetch historical events with filters
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
if (filter.category) params.set("category", filter.category);
|
|
63
|
+
if (filter.level) params.set("level", filter.level);
|
|
64
|
+
if (filter.agent_id) params.set("agent_id", filter.agent_id);
|
|
65
|
+
params.set("limit", "50");
|
|
66
|
+
|
|
67
|
+
const eventsRes = await fetch(`/api/telemetry/events?${params}`);
|
|
68
|
+
const eventsData = await eventsRes.json();
|
|
69
|
+
setHistoricalEvents(eventsData.events || []);
|
|
70
|
+
|
|
71
|
+
// Fetch usage by agent
|
|
72
|
+
const usageRes = await fetch("/api/telemetry/usage?group_by=agent");
|
|
73
|
+
const usageData = await usageRes.json();
|
|
74
|
+
setUsage(usageData.usage || []);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error("Failed to fetch telemetry:", e);
|
|
77
|
+
}
|
|
78
|
+
setLoading(false);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
fetchData();
|
|
83
|
+
// Refresh stats every 60 seconds (events come in real-time)
|
|
84
|
+
const interval = setInterval(fetchData, 60000);
|
|
85
|
+
return () => clearInterval(interval);
|
|
86
|
+
}, [filter]);
|
|
87
|
+
|
|
88
|
+
// Merge real-time events with historical, filtering and deduping
|
|
89
|
+
const allEvents = React.useMemo(() => {
|
|
90
|
+
// Apply filters to real-time events
|
|
91
|
+
let filtered = realtimeEvents;
|
|
92
|
+
if (filter.agent_id) {
|
|
93
|
+
filtered = filtered.filter(e => e.agent_id === filter.agent_id);
|
|
94
|
+
}
|
|
95
|
+
if (filter.category) {
|
|
96
|
+
filtered = filtered.filter(e => e.category === filter.category);
|
|
97
|
+
}
|
|
98
|
+
if (filter.level) {
|
|
99
|
+
filtered = filtered.filter(e => e.level === filter.level);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Merge with historical, dedupe by ID
|
|
103
|
+
const seen = new Set(filtered.map(e => e.id));
|
|
104
|
+
const merged = [...filtered];
|
|
105
|
+
for (const evt of historicalEvents) {
|
|
106
|
+
if (!seen.has(evt.id)) {
|
|
107
|
+
merged.push(evt);
|
|
108
|
+
seen.add(evt.id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sort by timestamp descending
|
|
113
|
+
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
114
|
+
|
|
115
|
+
return merged.slice(0, 100);
|
|
116
|
+
}, [realtimeEvents, historicalEvents, filter]);
|
|
117
|
+
|
|
118
|
+
const getAgentName = (agentId: string) => {
|
|
119
|
+
const agent = agents.find(a => a.id === agentId);
|
|
120
|
+
return agent?.name || agentId;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const formatNumber = (n: number) => {
|
|
124
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
125
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
126
|
+
return n.toString();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const levelColors: Record<string, string> = {
|
|
130
|
+
debug: "text-[#555]",
|
|
131
|
+
info: "text-blue-400",
|
|
132
|
+
warn: "text-yellow-400",
|
|
133
|
+
error: "text-red-400",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const categoryColors: Record<string, string> = {
|
|
137
|
+
LLM: "bg-purple-500/20 text-purple-400 border-purple-500/30",
|
|
138
|
+
TOOL: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
|
139
|
+
CHAT: "bg-green-500/20 text-green-400 border-green-500/30",
|
|
140
|
+
ERROR: "bg-red-500/20 text-red-400 border-red-500/30",
|
|
141
|
+
SYSTEM: "bg-gray-500/20 text-gray-400 border-gray-500/30",
|
|
142
|
+
TASK: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
|
143
|
+
MEMORY: "bg-cyan-500/20 text-cyan-400 border-cyan-500/30",
|
|
144
|
+
MCP: "bg-orange-500/20 text-orange-400 border-orange-500/30",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const agentOptions = [
|
|
148
|
+
{ value: "", label: "All Agents" },
|
|
149
|
+
...agents.map(a => ({ value: a.id, label: a.name })),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const categoryOptions = [
|
|
153
|
+
{ value: "", label: "All Categories" },
|
|
154
|
+
{ value: "LLM", label: "LLM" },
|
|
155
|
+
{ value: "TOOL", label: "Tool" },
|
|
156
|
+
{ value: "CHAT", label: "Chat" },
|
|
157
|
+
{ value: "TASK", label: "Task" },
|
|
158
|
+
{ value: "MEMORY", label: "Memory" },
|
|
159
|
+
{ value: "MCP", label: "MCP" },
|
|
160
|
+
{ value: "SYSTEM", label: "System" },
|
|
161
|
+
{ value: "ERROR", label: "Error" },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const levelOptions = [
|
|
165
|
+
{ value: "", label: "All Levels" },
|
|
166
|
+
{ value: "debug", label: "Debug" },
|
|
167
|
+
{ value: "info", label: "Info" },
|
|
168
|
+
{ value: "warn", label: "Warn" },
|
|
169
|
+
{ value: "error", label: "Error" },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="flex-1 overflow-auto p-6">
|
|
174
|
+
<div className="max-w-6xl">
|
|
175
|
+
{/* Header */}
|
|
176
|
+
<div className="mb-6 flex items-center justify-between">
|
|
177
|
+
<div>
|
|
178
|
+
<h1 className="text-2xl font-semibold mb-1">Telemetry</h1>
|
|
179
|
+
<p className="text-[#666]">
|
|
180
|
+
Monitor agent activity, token usage, and errors.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<span
|
|
185
|
+
className={`w-2 h-2 rounded-full ${connected ? "bg-green-400" : "bg-red-400"}`}
|
|
186
|
+
/>
|
|
187
|
+
<span className="text-xs text-[#666]">
|
|
188
|
+
{connected ? "Live" : "Reconnecting..."}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Stats Cards */}
|
|
194
|
+
{stats && (
|
|
195
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
196
|
+
<StatCard label="Events" value={formatNumber(stats.total_events)} />
|
|
197
|
+
<StatCard label="LLM Calls" value={formatNumber(stats.total_llm_calls)} />
|
|
198
|
+
<StatCard label="Tool Calls" value={formatNumber(stats.total_tool_calls)} />
|
|
199
|
+
<StatCard label="Errors" value={formatNumber(stats.total_errors)} color="red" />
|
|
200
|
+
<StatCard label="Input Tokens" value={formatNumber(stats.total_input_tokens)} />
|
|
201
|
+
<StatCard label="Output Tokens" value={formatNumber(stats.total_output_tokens)} />
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* Usage by Agent */}
|
|
206
|
+
{usage.length > 0 && (
|
|
207
|
+
<div className="mb-6">
|
|
208
|
+
<h2 className="text-lg font-medium mb-3">Usage by Agent</h2>
|
|
209
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg overflow-hidden">
|
|
210
|
+
<table className="w-full text-sm">
|
|
211
|
+
<thead>
|
|
212
|
+
<tr className="border-b border-[#1a1a1a] text-[#666]">
|
|
213
|
+
<th className="text-left p-3">Agent</th>
|
|
214
|
+
<th className="text-right p-3">LLM Calls</th>
|
|
215
|
+
<th className="text-right p-3">Tool Calls</th>
|
|
216
|
+
<th className="text-right p-3">Input Tokens</th>
|
|
217
|
+
<th className="text-right p-3">Output Tokens</th>
|
|
218
|
+
<th className="text-right p-3">Errors</th>
|
|
219
|
+
</tr>
|
|
220
|
+
</thead>
|
|
221
|
+
<tbody>
|
|
222
|
+
{usage.map((u) => (
|
|
223
|
+
<tr key={u.agent_id} className="border-b border-[#1a1a1a] last:border-0">
|
|
224
|
+
<td className="p-3 font-medium">{getAgentName(u.agent_id)}</td>
|
|
225
|
+
<td className="p-3 text-right text-[#888]">{formatNumber(u.llm_calls)}</td>
|
|
226
|
+
<td className="p-3 text-right text-[#888]">{formatNumber(u.tool_calls)}</td>
|
|
227
|
+
<td className="p-3 text-right text-[#888]">{formatNumber(u.input_tokens)}</td>
|
|
228
|
+
<td className="p-3 text-right text-[#888]">{formatNumber(u.output_tokens)}</td>
|
|
229
|
+
<td className="p-3 text-right">
|
|
230
|
+
{u.errors > 0 ? (
|
|
231
|
+
<span className="text-red-400">{u.errors}</span>
|
|
232
|
+
) : (
|
|
233
|
+
<span className="text-[#444]">0</span>
|
|
234
|
+
)}
|
|
235
|
+
</td>
|
|
236
|
+
</tr>
|
|
237
|
+
))}
|
|
238
|
+
</tbody>
|
|
239
|
+
</table>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* Filters */}
|
|
245
|
+
<div className="flex items-center gap-3 mb-4">
|
|
246
|
+
<div className="w-56">
|
|
247
|
+
<Select
|
|
248
|
+
value={filter.agent_id}
|
|
249
|
+
options={agentOptions}
|
|
250
|
+
onChange={(value) => setFilter({ ...filter, agent_id: value })}
|
|
251
|
+
placeholder="All Agents"
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="w-48">
|
|
255
|
+
<Select
|
|
256
|
+
value={filter.category}
|
|
257
|
+
options={categoryOptions}
|
|
258
|
+
onChange={(value) => setFilter({ ...filter, category: value })}
|
|
259
|
+
placeholder="All Categories"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="w-40">
|
|
263
|
+
<Select
|
|
264
|
+
value={filter.level}
|
|
265
|
+
options={levelOptions}
|
|
266
|
+
onChange={(value) => setFilter({ ...filter, level: value })}
|
|
267
|
+
placeholder="All Levels"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
<button
|
|
271
|
+
onClick={fetchData}
|
|
272
|
+
className="px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] border border-[#333] rounded text-sm transition"
|
|
273
|
+
>
|
|
274
|
+
Refresh
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Events List */}
|
|
279
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg">
|
|
280
|
+
<div className="p-3 border-b border-[#1a1a1a] flex items-center justify-between">
|
|
281
|
+
<h2 className="font-medium">Recent Events</h2>
|
|
282
|
+
{realtimeEvents.length > 0 && (
|
|
283
|
+
<span className="text-xs text-[#666]">
|
|
284
|
+
{realtimeEvents.length} new
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{loading && allEvents.length === 0 ? (
|
|
290
|
+
<div className="p-8 text-center text-[#666]">Loading...</div>
|
|
291
|
+
) : allEvents.length === 0 ? (
|
|
292
|
+
<div className="p-8 text-center text-[#666]">
|
|
293
|
+
No telemetry events yet. Events will appear here in real-time once agents start sending data.
|
|
294
|
+
</div>
|
|
295
|
+
) : (
|
|
296
|
+
<div className="divide-y divide-[#1a1a1a]">
|
|
297
|
+
{allEvents.map((event, index) => {
|
|
298
|
+
// Check if this is a new real-time event (in first few positions and recent)
|
|
299
|
+
const isNew = index < 3 && realtimeEvents.some(e => e.id === event.id);
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
key={event.id}
|
|
304
|
+
className={`p-3 hover:bg-[#0a0a0a] transition cursor-pointer ${
|
|
305
|
+
isNew ? "bg-[#0f1a0f]" : ""
|
|
306
|
+
}`}
|
|
307
|
+
onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
|
|
308
|
+
>
|
|
309
|
+
<div className="flex items-start gap-3">
|
|
310
|
+
<span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
|
|
311
|
+
{event.category}
|
|
312
|
+
</span>
|
|
313
|
+
<div className="flex-1 min-w-0">
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
315
|
+
<span className="font-medium text-sm">{event.type}</span>
|
|
316
|
+
<span className={`text-xs ${levelColors[event.level] || "text-[#666]"}`}>
|
|
317
|
+
{event.level}
|
|
318
|
+
</span>
|
|
319
|
+
{event.duration_ms && (
|
|
320
|
+
<span className="text-xs text-[#555]">{event.duration_ms}ms</span>
|
|
321
|
+
)}
|
|
322
|
+
{isNew && (
|
|
323
|
+
<span className="text-xs text-green-400">new</span>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
<div className="text-xs text-[#555] mt-1">
|
|
327
|
+
{getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
|
|
328
|
+
</div>
|
|
329
|
+
{event.error && (
|
|
330
|
+
<div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
|
|
331
|
+
)}
|
|
332
|
+
{expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
|
|
333
|
+
<pre className="text-xs text-[#666] mt-2 p-2 bg-[#0a0a0a] rounded overflow-x-auto">
|
|
334
|
+
{JSON.stringify(event.data, null, 2)}
|
|
335
|
+
</pre>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
|
|
351
|
+
return (
|
|
352
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
353
|
+
<div className="text-[#666] text-xs mb-1">{label}</div>
|
|
354
|
+
<div className={`text-2xl font-semibold ${color === "red" ? "text-red-400" : ""}`}>
|
|
355
|
+
{value}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } 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
|
+
clearEvents: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TelemetryContext = createContext<TelemetryContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
const MAX_EVENTS = 200; // Keep last 200 events in memory
|
|
27
|
+
|
|
28
|
+
export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
29
|
+
const [connected, setConnected] = useState(false);
|
|
30
|
+
const [events, setEvents] = useState<TelemetryEvent[]>([]);
|
|
31
|
+
const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
|
|
32
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
33
|
+
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
const connect = useCallback(() => {
|
|
36
|
+
if (eventSourceRef.current) {
|
|
37
|
+
eventSourceRef.current.close();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const es = new EventSource("/api/telemetry/stream");
|
|
41
|
+
eventSourceRef.current = es;
|
|
42
|
+
|
|
43
|
+
es.onopen = () => {
|
|
44
|
+
setConnected(true);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
es.onmessage = (event) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(event.data);
|
|
50
|
+
|
|
51
|
+
// Handle connection message
|
|
52
|
+
if (data.connected) {
|
|
53
|
+
setConnected(true);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle array of events
|
|
58
|
+
if (Array.isArray(data)) {
|
|
59
|
+
setEvents(prev => {
|
|
60
|
+
const combined = [...data, ...prev];
|
|
61
|
+
return combined.slice(0, MAX_EVENTS);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Update last activity per agent
|
|
65
|
+
setLastActivityByAgent(prev => {
|
|
66
|
+
const updated = { ...prev };
|
|
67
|
+
for (const evt of data) {
|
|
68
|
+
const existing = updated[evt.agent_id];
|
|
69
|
+
if (!existing || new Date(evt.timestamp) > new Date(existing.timestamp)) {
|
|
70
|
+
updated[evt.agent_id] = {
|
|
71
|
+
timestamp: evt.timestamp,
|
|
72
|
+
category: evt.category,
|
|
73
|
+
type: evt.type,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return updated;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error("Failed to parse telemetry event:", e);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
es.onerror = () => {
|
|
86
|
+
setConnected(false);
|
|
87
|
+
es.close();
|
|
88
|
+
eventSourceRef.current = null;
|
|
89
|
+
|
|
90
|
+
// Reconnect after 3 seconds
|
|
91
|
+
if (reconnectTimeoutRef.current) {
|
|
92
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
93
|
+
}
|
|
94
|
+
reconnectTimeoutRef.current = setTimeout(connect, 3000);
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
connect();
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
if (eventSourceRef.current) {
|
|
103
|
+
eventSourceRef.current.close();
|
|
104
|
+
}
|
|
105
|
+
if (reconnectTimeoutRef.current) {
|
|
106
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}, [connect]);
|
|
110
|
+
|
|
111
|
+
const clearEvents = useCallback(() => {
|
|
112
|
+
setEvents([]);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, clearEvents }}>
|
|
117
|
+
{children}
|
|
118
|
+
</TelemetryContext.Provider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Hook to access all telemetry
|
|
123
|
+
export function useTelemetryContext() {
|
|
124
|
+
const context = useContext(TelemetryContext);
|
|
125
|
+
if (!context) {
|
|
126
|
+
throw new Error("useTelemetryContext must be used within TelemetryProvider");
|
|
127
|
+
}
|
|
128
|
+
return context;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Hook to filter telemetry for a specific agent or category
|
|
132
|
+
export function useTelemetry(filter?: {
|
|
133
|
+
agent_id?: string;
|
|
134
|
+
category?: string;
|
|
135
|
+
limit?: number;
|
|
136
|
+
}) {
|
|
137
|
+
const { connected, events, lastActivityByAgent } = useTelemetryContext();
|
|
138
|
+
|
|
139
|
+
const filteredEvents = React.useMemo(() => {
|
|
140
|
+
let result = events;
|
|
141
|
+
|
|
142
|
+
if (filter?.agent_id) {
|
|
143
|
+
result = result.filter(e => e.agent_id === filter.agent_id);
|
|
144
|
+
}
|
|
145
|
+
if (filter?.category) {
|
|
146
|
+
result = result.filter(e => e.category === filter.category);
|
|
147
|
+
}
|
|
148
|
+
if (filter?.limit) {
|
|
149
|
+
result = result.slice(0, filter.limit);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}, [events, filter?.agent_id, filter?.category, filter?.limit]);
|
|
154
|
+
|
|
155
|
+
const lastActivity = filter?.agent_id ? lastActivityByAgent[filter.agent_id] : undefined;
|
|
156
|
+
|
|
157
|
+
// Check if agent is "active" (had activity in last 10 seconds)
|
|
158
|
+
const isActive = React.useMemo(() => {
|
|
159
|
+
if (!lastActivity) return false;
|
|
160
|
+
const activityTime = new Date(lastActivity.timestamp).getTime();
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
return now - activityTime < 10000; // 10 seconds
|
|
163
|
+
}, [lastActivity]);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
connected,
|
|
167
|
+
events: filteredEvents,
|
|
168
|
+
lastActivity,
|
|
169
|
+
isActive,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Hook for agent activity indicator
|
|
174
|
+
export function useAgentActivity(agentId: string) {
|
|
175
|
+
const { lastActivityByAgent } = useTelemetryContext();
|
|
176
|
+
const [isActive, setIsActive] = useState(false);
|
|
177
|
+
const lastActivity = lastActivityByAgent[agentId];
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!lastActivity) {
|
|
181
|
+
setIsActive(false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Set active when we get new activity
|
|
186
|
+
setIsActive(true);
|
|
187
|
+
|
|
188
|
+
// Clear active state after 3 seconds of no activity
|
|
189
|
+
const timeout = setTimeout(() => {
|
|
190
|
+
setIsActive(false);
|
|
191
|
+
}, 3000);
|
|
192
|
+
|
|
193
|
+
return () => clearTimeout(timeout);
|
|
194
|
+
}, [lastActivity?.timestamp]);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
isActive,
|
|
198
|
+
lastActivity,
|
|
199
|
+
category: lastActivity?.category,
|
|
200
|
+
type: lastActivity?.type,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -24,6 +24,7 @@ export function useAgents(enabled: boolean) {
|
|
|
24
24
|
provider: string;
|
|
25
25
|
systemPrompt: string;
|
|
26
26
|
features: AgentFeatures;
|
|
27
|
+
mcpServers?: string[];
|
|
27
28
|
}) => {
|
|
28
29
|
await fetch("/api/agents", {
|
|
29
30
|
method: "POST",
|
|
@@ -38,6 +39,27 @@ export function useAgents(enabled: boolean) {
|
|
|
38
39
|
await fetchAgents();
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
const updateAgent = async (id: string, updates: {
|
|
43
|
+
name?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
provider?: string;
|
|
46
|
+
systemPrompt?: string;
|
|
47
|
+
features?: AgentFeatures;
|
|
48
|
+
mcpServers?: string[];
|
|
49
|
+
}): Promise<{ error?: string }> => {
|
|
50
|
+
const res = await fetch(`/api/agents/${id}`, {
|
|
51
|
+
method: "PUT",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify(updates),
|
|
54
|
+
});
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
await fetchAgents();
|
|
57
|
+
if (!res.ok && data.error) {
|
|
58
|
+
return { error: data.error };
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
};
|
|
62
|
+
|
|
41
63
|
const toggleAgent = async (agent: Agent): Promise<{ error?: string }> => {
|
|
42
64
|
const action = agent.status === "running" ? "stop" : "start";
|
|
43
65
|
const res = await fetch(`/api/agents/${agent.id}/${action}`, { method: "POST" });
|
|
@@ -57,6 +79,7 @@ export function useAgents(enabled: boolean) {
|
|
|
57
79
|
runningCount,
|
|
58
80
|
fetchAgents,
|
|
59
81
|
createAgent,
|
|
82
|
+
updateAgent,
|
|
60
83
|
deleteAgent,
|
|
61
84
|
toggleAgent,
|
|
62
85
|
};
|
package/src/web/styles.css
CHANGED
|
@@ -10,6 +10,15 @@ html, body {
|
|
|
10
10
|
-moz-osx-font-smoothing: grayscale;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/* Hide scrollbars globally but allow scrolling */
|
|
14
|
+
* {
|
|
15
|
+
-ms-overflow-style: none;
|
|
16
|
+
scrollbar-width: none;
|
|
17
|
+
}
|
|
18
|
+
*::-webkit-scrollbar {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
::selection {
|
|
14
23
|
background-color: #f97316;
|
|
15
24
|
color: #0a0a0a;
|
|
@@ -21,3 +30,12 @@ html, body {
|
|
|
21
30
|
-webkit-box-orient: vertical;
|
|
22
31
|
overflow: hidden;
|
|
23
32
|
}
|
|
33
|
+
|
|
34
|
+
/* Hide scrollbar but allow scrolling */
|
|
35
|
+
.scrollbar-hide {
|
|
36
|
+
-ms-overflow-style: none;
|
|
37
|
+
scrollbar-width: none;
|
|
38
|
+
}
|
|
39
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|