apteva 0.4.31 → 0.4.41
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/ActivityPage.7907h64p.js +3 -0
- package/dist/ApiDocsPage.k3jjenpq.js +4 -0
- package/dist/App.01nq20st.js +4 -0
- package/dist/App.1maqvamf.js +4 -0
- package/dist/App.2yjrh32f.js +4 -0
- package/dist/App.3qw8nben.js +20 -0
- package/dist/App.7fb3e7mp.js +4 -0
- package/dist/App.7sy3wq8c.js +4 -0
- package/dist/App.apjrmctz.js +57 -0
- package/dist/App.av6t2yhe.js +4 -0
- package/dist/App.jqj5a094.js +46 -0
- package/dist/App.mc7xf85h.js +4 -0
- package/dist/App.myxqcj9x.js +4 -0
- package/dist/App.nm91r1mp.js +13 -0
- package/dist/App.qcknavjz.js +221 -0
- package/dist/App.vc7vfhg4.js +4 -0
- package/dist/App.z4s9zkw5.js +4 -0
- package/dist/ConnectionsPage.z1pw5xe2.js +3 -0
- package/dist/McpPage.8vc97z0b.js +3 -0
- package/dist/SettingsPage.p61bz8kd.js +3 -0
- package/dist/SkillsPage.r9x43g3g.js +3 -0
- package/dist/TasksPage.1e0zkye4.js +3 -0
- package/dist/TelemetryPage.p9vbe4gf.js +3 -0
- package/dist/TestsPage.d4xy504e.js +3 -0
- package/dist/ThreadsPage.m016am3x.js +3 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -7
- package/src/crypto.ts +4 -3
- package/src/db.ts +153 -28
- package/src/integrations/agentdojo.ts +94 -12
- package/src/integrations/index.ts +7 -0
- package/src/mcp-platform.ts +494 -121
- package/src/providers.ts +12 -12
- package/src/routes/api/agent-utils.ts +59 -46
- package/src/routes/api/agents.ts +52 -1
- package/src/routes/api/integrations.ts +11 -5
- package/src/routes/api/mcp.ts +5 -4
- package/src/routes/api/meta-agent.ts +35 -1
- package/src/routes/api/projects.ts +3 -3
- package/src/routes/api/providers.ts +121 -30
- package/src/routes/api/skills.ts +2 -3
- package/src/routes/api/system.ts +8 -13
- package/src/server.ts +31 -32
- package/src/triggers/agentdojo.ts +2 -2
- package/src/web/App.tsx +18 -10
- package/src/web/components/activity/ActivityPage.tsx +241 -388
- package/src/web/components/agents/AgentCard.tsx +5 -13
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Select.tsx +4 -3
- package/src/web/components/dashboard/Dashboard.tsx +155 -30
- package/src/web/components/index.ts +1 -1
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +126 -35
- package/src/web/components/mcp/McpPage.tsx +10 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +4 -2
- package/src/web/components/settings/SettingsPage.tsx +133 -48
- package/src/web/components/tasks/TasksPage.tsx +48 -16
- package/src/web/components/telemetry/TelemetryPage.tsx +184 -0
- package/src/web/components/threads/ThreadsPage.tsx +313 -0
- package/src/web/context/AuthContext.tsx +3 -3
- package/src/web/context/ProjectContext.tsx +3 -3
- package/src/web/context/TelemetryContext.tsx +24 -6
- package/src/web/context/index.ts +1 -1
- package/src/web/styles.css +20 -4
- package/src/web/types.ts +4 -3
- package/dist/ActivityPage.41nbye4r.js +0 -3
- package/dist/ApiDocsPage.4smnt8m3.js +0 -4
- package/dist/App.0sbax9et.js +0 -4
- package/dist/App.0ws427h8.js +0 -4
- package/dist/App.6q6bar8b.js +0 -4
- package/dist/App.80301vdb.js +0 -4
- package/dist/App.af2wg84v.js +0 -267
- package/dist/App.ca1rz1ph.js +0 -4
- package/dist/App.ensa6z0r.js +0 -4
- package/dist/App.f8g7tych.js +0 -13
- package/dist/App.mvtqv6qc.js +0 -20
- package/dist/App.ncgc9cxy.js +0 -4
- package/dist/App.p0fb1pds.js +0 -4
- package/dist/App.pmaq48sj.js +0 -4
- package/dist/App.yv87t9m5.js +0 -4
- package/dist/App.zjmfm8p6.js +0 -4
- package/dist/ConnectionsPage.anb3rv9a.js +0 -3
- package/dist/McpPage.y396h6fy.js +0 -3
- package/dist/SettingsPage.p1hc60gk.js +0 -3
- package/dist/SkillsPage.yj3xdsay.js +0 -3
- package/dist/TasksPage.sjv0khtv.js +0 -3
- package/dist/TelemetryPage.2qm4w16r.js +0 -3
- package/dist/TestsPage.zzs4qfj8.js +0 -3
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { useTelemetry } from "../../context/TelemetryContext";
|
|
2
|
+
import { useAuth, useProjects, useTelemetryContext } from "../../context";
|
|
4
3
|
import type { TelemetryEvent } from "../../context";
|
|
5
|
-
import type { Agent,
|
|
6
|
-
import {
|
|
4
|
+
import type { Agent, Route } from "../../types";
|
|
5
|
+
import { Select } from "../common/Select";
|
|
7
6
|
|
|
8
7
|
interface ActivityPageProps {
|
|
9
8
|
agents: Agent[];
|
|
@@ -11,15 +10,97 @@ interface ActivityPageProps {
|
|
|
11
10
|
onNavigate?: (route: Route) => void;
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
// Event types we show in the timeline (skip noisy internal ones)
|
|
14
|
+
const VISIBLE_TYPES = new Set([
|
|
15
|
+
"thread_activity",
|
|
16
|
+
"agent_started",
|
|
17
|
+
"agent_stopped",
|
|
18
|
+
"agent_error",
|
|
19
|
+
"task_created",
|
|
20
|
+
"task_updated",
|
|
21
|
+
"task_deleted",
|
|
22
|
+
"task_executed",
|
|
23
|
+
"llm_request",
|
|
24
|
+
"tool_invocation",
|
|
25
|
+
"mcp_request",
|
|
26
|
+
"mcp_tool_execution",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// Category colors for the timeline dot
|
|
30
|
+
const CATEGORY_COLORS: Record<string, string> = {
|
|
31
|
+
CHAT: "bg-green-400",
|
|
32
|
+
LLM: "bg-purple-400",
|
|
33
|
+
TOOL: "bg-blue-400",
|
|
34
|
+
TASK: "bg-yellow-400",
|
|
35
|
+
MEMORY: "bg-cyan-400",
|
|
36
|
+
MCP: "bg-orange-400",
|
|
37
|
+
SYSTEM: "bg-gray-400",
|
|
38
|
+
ERROR: "bg-red-400",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function describeEvent(evt: TelemetryEvent, agentName: string): string {
|
|
42
|
+
const data = evt.data || {};
|
|
43
|
+
switch (evt.type) {
|
|
44
|
+
case "thread_activity":
|
|
45
|
+
return (data.activity as string) || "Working...";
|
|
46
|
+
case "agent_started":
|
|
47
|
+
return "Agent started";
|
|
48
|
+
case "agent_stopped":
|
|
49
|
+
return data.reason ? `Agent stopped (${data.reason})` : "Agent stopped";
|
|
50
|
+
case "agent_error":
|
|
51
|
+
return evt.error || "Agent error";
|
|
52
|
+
case "llm_request":
|
|
53
|
+
return "Thinking...";
|
|
54
|
+
case "tool_invocation": {
|
|
55
|
+
const toolRaw = (data.tool_name || "") as string;
|
|
56
|
+
if (!toolRaw) return "Using tools";
|
|
57
|
+
const toolFormatted = toolRaw.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
|
58
|
+
return `Tool: ${toolFormatted}`;
|
|
59
|
+
}
|
|
60
|
+
case "task_created":
|
|
61
|
+
return data.title ? `Task created: ${data.title}` : "Task created";
|
|
62
|
+
case "task_updated": {
|
|
63
|
+
const status = data.status as string | undefined;
|
|
64
|
+
const title = data.title as string | undefined;
|
|
65
|
+
const statusLabel = status ? status.charAt(0).toUpperCase() + status.slice(1) : null;
|
|
66
|
+
if (title && statusLabel) return `Task ${statusLabel}: ${title}`;
|
|
67
|
+
if (statusLabel) return `Task ${statusLabel}`;
|
|
68
|
+
if (title) return `Task updated: ${title}`;
|
|
69
|
+
return "Task updated";
|
|
70
|
+
}
|
|
71
|
+
case "task_deleted":
|
|
72
|
+
return "Task deleted";
|
|
73
|
+
case "task_executed": {
|
|
74
|
+
const title = data.title as string | undefined;
|
|
75
|
+
return title ? `Task started: ${title}` : "Task started";
|
|
76
|
+
}
|
|
77
|
+
case "memory_stored":
|
|
78
|
+
return "Memory stored";
|
|
79
|
+
case "memory_retrieved":
|
|
80
|
+
return "Memory retrieved";
|
|
81
|
+
case "mcp_request":
|
|
82
|
+
return data.server ? `MCP request to ${data.server}` : "MCP request";
|
|
83
|
+
case "mcp_tool_execution": {
|
|
84
|
+
const rawName = (data.tool_name || data.tool || "") as string;
|
|
85
|
+
if (!rawName) return "MCP tool call";
|
|
86
|
+
// "Server__tool-name-here" -> take part after __, format dashes/underscores to spaces, title case
|
|
87
|
+
const toolPart = rawName.includes("__") ? rawName.split("__").slice(1).join("__") : rawName;
|
|
88
|
+
const formatted = toolPart.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
|
89
|
+
return `MCP: ${formatted}`;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return evt.type.replace(/_/g, " ");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
14
96
|
export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps) {
|
|
15
97
|
const { authFetch } = useAuth();
|
|
16
98
|
const { currentProjectId } = useProjects();
|
|
17
99
|
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
18
|
-
const [
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
21
|
-
const
|
|
22
|
-
const { events: taskTelemetryEvents } = useTelemetry({ category: "TASK" });
|
|
100
|
+
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
101
|
+
const [filterAgentId, setFilterAgentId] = useState<string>("");
|
|
102
|
+
const [seenIds] = useState(() => new Set<string>());
|
|
103
|
+
const hasLoadedHistory = useRef(false);
|
|
23
104
|
|
|
24
105
|
const filteredAgents = useMemo(() => {
|
|
25
106
|
if (currentProjectId === null) return agents;
|
|
@@ -27,16 +108,6 @@ export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps)
|
|
|
27
108
|
return agents.filter(a => a.projectId === currentProjectId);
|
|
28
109
|
}, [agents, currentProjectId]);
|
|
29
110
|
|
|
30
|
-
const sortedAgents = useMemo(() => {
|
|
31
|
-
return [...filteredAgents].sort((a, b) => {
|
|
32
|
-
if (a.status === "running" && b.status !== "running") return -1;
|
|
33
|
-
if (b.status === "running" && a.status !== "running") return 1;
|
|
34
|
-
return a.name.localeCompare(b.name);
|
|
35
|
-
});
|
|
36
|
-
}, [filteredAgents]);
|
|
37
|
-
|
|
38
|
-
const runningCount = useMemo(() => filteredAgents.filter(a => a.status === "running").length, [filteredAgents]);
|
|
39
|
-
|
|
40
111
|
const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
41
112
|
const agentNameMap = useMemo(() => {
|
|
42
113
|
const map = new Map<string, string>();
|
|
@@ -44,410 +115,192 @@ export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps)
|
|
|
44
115
|
return map;
|
|
45
116
|
}, [filteredAgents]);
|
|
46
117
|
|
|
47
|
-
// Fetch
|
|
48
|
-
const
|
|
118
|
+
// Fetch historical events
|
|
119
|
+
const fetchHistory = useCallback(async () => {
|
|
49
120
|
const projectParam = currentProjectId ? `&project_id=${encodeURIComponent(currentProjectId)}` : "";
|
|
50
|
-
|
|
51
|
-
authFetch(`/api/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
list.sort((a, b) => {
|
|
58
|
-
const aPri = a.status === "running" ? 0 : a.status === "pending" ? 1 : a.status === "completed" ? 2 : 3;
|
|
59
|
-
const bPri = b.status === "running" ? 0 : b.status === "pending" ? 1 : b.status === "completed" ? 2 : 3;
|
|
60
|
-
if (aPri !== bPri) return aPri - bPri;
|
|
61
|
-
if (aPri <= 1) {
|
|
62
|
-
const aTs = (a.next_run || a.execute_at) ? new Date(a.next_run || a.execute_at!).getTime() : Infinity;
|
|
63
|
-
const bTs = (b.next_run || b.execute_at) ? new Date(b.next_run || b.execute_at!).getTime() : Infinity;
|
|
64
|
-
return aTs - bTs;
|
|
65
|
-
}
|
|
66
|
-
const aDate = a.completed_at || a.executed_at || a.created_at;
|
|
67
|
-
const bDate = b.completed_at || b.executed_at || b.created_at;
|
|
68
|
-
return new Date(bDate).getTime() - new Date(aDate).getTime();
|
|
69
|
-
});
|
|
70
|
-
setTasks(list);
|
|
71
|
-
}
|
|
72
|
-
if (activityRes?.ok) {
|
|
73
|
-
const data = await activityRes.json();
|
|
74
|
-
setHistoricalActivities(data.events || []);
|
|
75
|
-
}
|
|
121
|
+
try {
|
|
122
|
+
const res = await authFetch(`/api/telemetry/events?limit=200${projectParam}`);
|
|
123
|
+
if (res.ok) {
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
setHistoricalEvents(data.events || []);
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
76
128
|
}, [authFetch, currentProjectId]);
|
|
77
129
|
|
|
78
130
|
useEffect(() => {
|
|
79
|
-
|
|
80
|
-
}, [
|
|
131
|
+
fetchHistory();
|
|
132
|
+
}, [fetchHistory, statusChangeCounter]);
|
|
81
133
|
|
|
82
|
-
//
|
|
134
|
+
// Mark historical events as seen so they don't animate
|
|
83
135
|
useEffect(() => {
|
|
84
|
-
if (!
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
if (historicalEvents.length > 0 && !hasLoadedHistory.current) {
|
|
137
|
+
for (const evt of historicalEvents) {
|
|
138
|
+
seenIds.add(evt.id);
|
|
139
|
+
}
|
|
140
|
+
// Also mark any realtime events already present at mount
|
|
141
|
+
for (const evt of realtimeEvents) {
|
|
142
|
+
seenIds.add(evt.id);
|
|
143
|
+
}
|
|
144
|
+
hasLoadedHistory.current = true;
|
|
91
145
|
}
|
|
92
|
-
}, [
|
|
146
|
+
}, [historicalEvents, realtimeEvents, seenIds]);
|
|
147
|
+
|
|
148
|
+
// Merge realtime + historical, filter, sort
|
|
149
|
+
const timeline = useMemo(() => {
|
|
150
|
+
const seen = new Set<string>();
|
|
151
|
+
const merged: TelemetryEvent[] = [];
|
|
152
|
+
|
|
153
|
+
const shouldShow = (evt: TelemetryEvent) => {
|
|
154
|
+
if (!VISIBLE_TYPES.has(evt.type) || !agentIds.has(evt.agent_id) || evt.data?.parent_id) return false;
|
|
155
|
+
// MCP tool: only show completed calls (with duration), skip the start event
|
|
156
|
+
if (evt.type === "mcp_tool_execution" && !evt.duration_ms) return false;
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
93
159
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity");
|
|
97
|
-
const seen = new Set(realtimeThreadEvents.map(e => e.id));
|
|
98
|
-
const merged = [...realtimeThreadEvents];
|
|
99
|
-
for (const evt of historicalActivities) {
|
|
100
|
-
if (!seen.has(evt.id)) {
|
|
160
|
+
for (const evt of realtimeEvents) {
|
|
161
|
+
if (!seen.has(evt.id) && shouldShow(evt)) {
|
|
101
162
|
merged.push(evt);
|
|
102
163
|
seen.add(evt.id);
|
|
103
164
|
}
|
|
104
165
|
}
|
|
105
|
-
|
|
166
|
+
for (const evt of historicalEvents) {
|
|
167
|
+
if (!seen.has(evt.id) && shouldShow(evt)) {
|
|
168
|
+
merged.push(evt);
|
|
169
|
+
seen.add(evt.id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let filtered = merged;
|
|
174
|
+
if (filterAgentId) {
|
|
175
|
+
filtered = filtered.filter(e => e.agent_id === filterAgentId);
|
|
176
|
+
}
|
|
177
|
+
|
|
106
178
|
filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
107
|
-
return filtered.slice(0,
|
|
108
|
-
}, [realtimeEvents,
|
|
179
|
+
return filtered.slice(0, 200);
|
|
180
|
+
}, [realtimeEvents, historicalEvents, agentIds, filterAgentId]);
|
|
181
|
+
|
|
182
|
+
// Group by date for section headers
|
|
183
|
+
const groupedTimeline = useMemo(() => {
|
|
184
|
+
const groups: { label: string; events: TelemetryEvent[] }[] = [];
|
|
185
|
+
let currentLabel = "";
|
|
186
|
+
const today = new Date().toDateString();
|
|
187
|
+
const yesterday = new Date(Date.now() - 86400000).toDateString();
|
|
188
|
+
|
|
189
|
+
for (const evt of timeline) {
|
|
190
|
+
const dateStr = new Date(evt.timestamp).toDateString();
|
|
191
|
+
const label = dateStr === today ? "Today" : dateStr === yesterday ? "Yesterday" : dateStr;
|
|
192
|
+
if (label !== currentLabel) {
|
|
193
|
+
currentLabel = label;
|
|
194
|
+
groups.push({ label, events: [] });
|
|
195
|
+
}
|
|
196
|
+
groups[groups.length - 1].events.push(evt);
|
|
197
|
+
}
|
|
198
|
+
return groups;
|
|
199
|
+
}, [timeline]);
|
|
109
200
|
|
|
110
201
|
if (loading) {
|
|
111
202
|
return <div className="flex-1 flex items-center justify-center text-[#666]">Loading...</div>;
|
|
112
203
|
}
|
|
113
204
|
|
|
205
|
+
const runningCount = filteredAgents.filter(a => a.status === "running").length;
|
|
206
|
+
|
|
114
207
|
return (
|
|
115
208
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
116
209
|
{/* Header */}
|
|
117
210
|
<div className="px-6 pt-6 pb-4 shrink-0">
|
|
118
|
-
<div className="flex items-center justify-between">
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{/* Three-column layout: 1/4 | 3/8 | 3/8 */}
|
|
127
|
-
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
128
|
-
{/* Left: Agents (1/4) */}
|
|
129
|
-
<div className="flex-[2] flex flex-col overflow-hidden border-r border-[#1a1a1a]">
|
|
130
|
-
<div className="px-4 py-2.5 border-b border-[#1a1a1a] shrink-0">
|
|
131
|
-
<h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Agents</h3>
|
|
211
|
+
<div className="flex items-center justify-between gap-4">
|
|
212
|
+
<div>
|
|
213
|
+
<h1 className="text-xl font-semibold">Activity</h1>
|
|
214
|
+
<p className="text-sm text-[#666] mt-0.5">
|
|
215
|
+
{runningCount} of {filteredAgents.length} agents running
|
|
216
|
+
</p>
|
|
132
217
|
</div>
|
|
133
|
-
<div className="
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
onSelect={() => setSelectedAgentId(selectedAgentId === agent.id ? null : agent.id)}
|
|
144
|
-
/>
|
|
145
|
-
))}
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
|
|
149
|
-
{selectedAgentId && (
|
|
150
|
-
<InlineCommand
|
|
151
|
-
agent={filteredAgents.find(a => a.id === selectedAgentId) || null}
|
|
152
|
-
/>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
{/* Center: Activity Feed (3/8) */}
|
|
158
|
-
<div className="flex-[3] flex flex-col min-h-0 overflow-hidden border-r border-[#1a1a1a]">
|
|
159
|
-
<div className="px-4 py-2.5 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
160
|
-
<h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Activity Feed</h3>
|
|
161
|
-
<span className="text-xs text-[#555]">{activities.length}</span>
|
|
162
|
-
</div>
|
|
163
|
-
<div className="flex-1 overflow-auto">
|
|
164
|
-
{activities.length === 0 ? (
|
|
165
|
-
<div className="p-6 text-center text-[#555] text-sm">
|
|
166
|
-
No activity yet. Agent activity will appear here in real-time.
|
|
167
|
-
</div>
|
|
168
|
-
) : (
|
|
169
|
-
<div className="divide-y divide-[#1a1a1a]">
|
|
170
|
-
{activities.map(evt => (
|
|
171
|
-
<div key={evt.id} className="px-4 py-2.5 hover:bg-[#111]/50 transition">
|
|
172
|
-
<p className="text-sm truncate">{(evt.data?.activity as string) || "Working..."}</p>
|
|
173
|
-
<div className="flex items-center gap-2 text-[10px] text-[#555] mt-0.5">
|
|
174
|
-
<span className="text-[#666]">{agentNameMap.get(evt.agent_id) || evt.agent_id}</span>
|
|
175
|
-
<span className="text-[#444]">·</span>
|
|
176
|
-
<span>{timeAgo(evt.timestamp)}</span>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
))}
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
218
|
+
<div className="w-48">
|
|
219
|
+
<Select
|
|
220
|
+
value={filterAgentId}
|
|
221
|
+
onChange={setFilterAgentId}
|
|
222
|
+
placeholder="All agents"
|
|
223
|
+
options={[
|
|
224
|
+
{ value: "", label: "All agents" },
|
|
225
|
+
...filteredAgents.map(a => ({ value: a.id, label: a.name })),
|
|
226
|
+
]}
|
|
227
|
+
/>
|
|
182
228
|
</div>
|
|
183
229
|
</div>
|
|
230
|
+
</div>
|
|
184
231
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
View All
|
|
192
|
-
</button>
|
|
193
|
-
)}
|
|
232
|
+
{/* Timeline */}
|
|
233
|
+
<div className="flex-1 overflow-auto px-6 pb-6">
|
|
234
|
+
{timeline.length === 0 ? (
|
|
235
|
+
<div className="flex flex-col items-center justify-center py-20 text-[#555]">
|
|
236
|
+
<p className="text-lg mb-2">No activity yet</p>
|
|
237
|
+
<p className="text-sm">Agent activity will appear here in real-time.</p>
|
|
194
238
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
239
|
+
) : (
|
|
240
|
+
<div className="max-w-2xl">
|
|
241
|
+
{groupedTimeline.map(group => (
|
|
242
|
+
<div key={group.label}>
|
|
243
|
+
{/* Date header */}
|
|
244
|
+
<div className="sticky top-0 z-10 bg-[#0a0a0a]/95 backdrop-blur-sm py-2 mb-1">
|
|
245
|
+
<span className="text-xs font-semibold text-[#666] uppercase tracking-wider">
|
|
246
|
+
{group.label}
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Events */}
|
|
251
|
+
<div className="relative ml-3 border-l border-[#1a1a1a]">
|
|
252
|
+
{group.events.map(evt => {
|
|
253
|
+
const isNew = !seenIds.has(evt.id);
|
|
254
|
+
if (isNew) seenIds.add(evt.id); // mark seen after first render
|
|
255
|
+
const agentName = agentNameMap.get(evt.agent_id) || evt.agent_id.slice(0, 8);
|
|
256
|
+
const dotColor = evt.level === "error"
|
|
257
|
+
? "bg-red-400"
|
|
258
|
+
: CATEGORY_COLORS[evt.category] || "bg-[#555]";
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
key={evt.id}
|
|
263
|
+
className={`relative pl-6 pr-3 py-2.5 hover:bg-[#111]/50 transition-colors ${
|
|
264
|
+
isNew ? "animate-slideIn" : ""
|
|
265
|
+
}`}
|
|
266
|
+
>
|
|
267
|
+
{/* Timeline dot */}
|
|
268
|
+
<span className={`absolute left-[-4.5px] top-[14px] w-[9px] h-[9px] rounded-full ${dotColor} ring-2 ring-[#0a0a0a]`} />
|
|
269
|
+
|
|
270
|
+
<div className="flex items-start justify-between gap-3">
|
|
271
|
+
<div className="flex-1 min-w-0">
|
|
272
|
+
<p className={`text-sm ${evt.level === "error" ? "text-red-400" : ""}`}>
|
|
273
|
+
{describeEvent(evt, agentName)}
|
|
274
|
+
</p>
|
|
275
|
+
<div className="flex items-center gap-2 text-[11px] text-[#555] mt-0.5">
|
|
276
|
+
<span className="text-[#888] font-medium">{agentName}</span>
|
|
277
|
+
<span className="text-[#333]">·</span>
|
|
278
|
+
<span className="text-[#555]">{evt.category}</span>
|
|
279
|
+
{evt.duration_ms != null && evt.duration_ms > 0 && (
|
|
280
|
+
<>
|
|
281
|
+
<span className="text-[#333]">·</span>
|
|
282
|
+
<span>{evt.duration_ms < 1000 ? `${evt.duration_ms}ms` : `${(evt.duration_ms / 1000).toFixed(1)}s`}</span>
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<span className="text-[11px] text-[#555] shrink-0 pt-0.5">
|
|
288
|
+
{timeAgo(evt.timestamp)}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
})}
|
|
294
|
+
</div>
|
|
203
295
|
</div>
|
|
204
|
-
)}
|
|
296
|
+
))}
|
|
205
297
|
</div>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// --- Agent Row ---
|
|
213
|
-
|
|
214
|
-
function AgentRow({ agent, selected, onSelect }: {
|
|
215
|
-
agent: Agent;
|
|
216
|
-
selected: boolean;
|
|
217
|
-
onSelect: () => void;
|
|
218
|
-
}) {
|
|
219
|
-
const { isActive, label } = useAgentActivity(agent.id);
|
|
220
|
-
const isRunning = agent.status === "running";
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<button
|
|
224
|
-
onClick={onSelect}
|
|
225
|
-
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition ${
|
|
226
|
-
selected
|
|
227
|
-
? "bg-[#f97316]/10 border border-[#f97316]/30"
|
|
228
|
-
: "hover:bg-[#1a1a1a] border border-transparent"
|
|
229
|
-
}`}
|
|
230
|
-
>
|
|
231
|
-
<span
|
|
232
|
-
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
|
233
|
-
isRunning && isActive
|
|
234
|
-
? "bg-green-400 animate-pulse"
|
|
235
|
-
: isRunning
|
|
236
|
-
? "bg-[#3b82f6]"
|
|
237
|
-
: "bg-[#444]"
|
|
238
|
-
}`}
|
|
239
|
-
/>
|
|
240
|
-
<div className="flex-1 min-w-0">
|
|
241
|
-
<div className="flex items-center gap-2">
|
|
242
|
-
<span className={`text-sm font-medium truncate ${isRunning ? "" : "text-[#666]"}`}>
|
|
243
|
-
{agent.name}
|
|
244
|
-
</span>
|
|
245
|
-
<span className="text-[10px] text-[#555] shrink-0">{agent.provider}</span>
|
|
246
|
-
</div>
|
|
247
|
-
{isActive && label ? (
|
|
248
|
-
<p className="text-[11px] text-green-400 truncate">{label}</p>
|
|
249
|
-
) : (
|
|
250
|
-
<p className={`text-[11px] ${isRunning ? "text-[#555]" : "text-[#444]"}`}>
|
|
251
|
-
{isRunning ? "idle" : "stopped"}
|
|
252
|
-
</p>
|
|
253
|
-
)}
|
|
254
|
-
</div>
|
|
255
|
-
</button>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// --- Inline Command ---
|
|
260
|
-
|
|
261
|
-
function InlineCommand({ agent }: { agent: Agent | null }) {
|
|
262
|
-
const { authFetch } = useAuth();
|
|
263
|
-
const [command, setCommand] = useState("");
|
|
264
|
-
const [sending, setSending] = useState(false);
|
|
265
|
-
const [toast, setToast] = useState<string | null>(null);
|
|
266
|
-
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
setCommand("");
|
|
269
|
-
setToast(null);
|
|
270
|
-
}, [agent?.id]);
|
|
271
|
-
|
|
272
|
-
if (!agent) return null;
|
|
273
|
-
|
|
274
|
-
const isRunning = agent.status === "running";
|
|
275
|
-
|
|
276
|
-
const handleSend = async () => {
|
|
277
|
-
if (!command.trim() || sending) return;
|
|
278
|
-
if (!isRunning) {
|
|
279
|
-
setToast("Agent is not running");
|
|
280
|
-
setTimeout(() => setToast(null), 3000);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
setSending(true);
|
|
284
|
-
try {
|
|
285
|
-
const res = await authFetch(`/api/agents/${agent.id}/chat`, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
headers: { "Content-Type": "application/json" },
|
|
288
|
-
body: JSON.stringify({ message: command, agent_id: agent.id }),
|
|
289
|
-
});
|
|
290
|
-
if (res.ok) {
|
|
291
|
-
setToast("Sent");
|
|
292
|
-
setCommand("");
|
|
293
|
-
} else {
|
|
294
|
-
const data = await res.json().catch(() => ({}));
|
|
295
|
-
setToast(data.error || "Failed");
|
|
296
|
-
}
|
|
297
|
-
} catch {
|
|
298
|
-
setToast("Failed to send");
|
|
299
|
-
} finally {
|
|
300
|
-
setSending(false);
|
|
301
|
-
setTimeout(() => setToast(null), 3000);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<div className="mt-2 bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-2.5">
|
|
307
|
-
<div className="flex items-center justify-between mb-1.5">
|
|
308
|
-
<span className="text-[10px] text-[#666]">
|
|
309
|
-
Send to <span className="text-[#888]">{agent.name}</span>
|
|
310
|
-
</span>
|
|
311
|
-
{toast && (
|
|
312
|
-
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
|
|
313
|
-
toast === "Sent" ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
|
|
314
|
-
}`}>{toast}</span>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
<div className="flex gap-1.5">
|
|
318
|
-
<input
|
|
319
|
-
type="text"
|
|
320
|
-
value={command}
|
|
321
|
-
onChange={e => setCommand(e.target.value)}
|
|
322
|
-
onKeyDown={e => e.key === "Enter" && handleSend()}
|
|
323
|
-
placeholder={isRunning ? "Command..." : "Not running"}
|
|
324
|
-
disabled={sending || !isRunning}
|
|
325
|
-
autoFocus
|
|
326
|
-
className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-2 py-1.5 text-xs focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
|
|
327
|
-
/>
|
|
328
|
-
<button
|
|
329
|
-
onClick={handleSend}
|
|
330
|
-
disabled={sending || !command.trim() || !isRunning}
|
|
331
|
-
className="px-2.5 py-1.5 bg-[#f97316]/20 text-[#f97316] rounded text-xs font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
|
|
332
|
-
>
|
|
333
|
-
{sending ? "..." : "Send"}
|
|
334
|
-
</button>
|
|
335
|
-
</div>
|
|
336
|
-
</div>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// --- Task Card (matches TasksPage style) ---
|
|
341
|
-
|
|
342
|
-
const taskStatusColors: Record<string, string> = {
|
|
343
|
-
pending: "bg-yellow-500/20 text-yellow-400",
|
|
344
|
-
running: "bg-blue-500/20 text-blue-400",
|
|
345
|
-
completed: "bg-green-500/20 text-green-400",
|
|
346
|
-
failed: "bg-red-500/20 text-red-400",
|
|
347
|
-
cancelled: "bg-gray-500/20 text-gray-400",
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const TASK_DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
351
|
-
|
|
352
|
-
function TaskCard({ task }: { task: Task }) {
|
|
353
|
-
return (
|
|
354
|
-
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 hover:border-[#333] transition">
|
|
355
|
-
<div className="flex items-start justify-between mb-1.5">
|
|
356
|
-
<div className="flex-1 min-w-0">
|
|
357
|
-
<h4 className="text-sm font-medium truncate">{task.title}</h4>
|
|
358
|
-
<p className="text-xs text-[#666]">{task.agentName}</p>
|
|
359
|
-
</div>
|
|
360
|
-
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 ml-2 ${taskStatusColors[task.status] || taskStatusColors.pending}`}>
|
|
361
|
-
{task.status}
|
|
362
|
-
</span>
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
|
-
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#555]">
|
|
366
|
-
<span className="flex items-center gap-1">
|
|
367
|
-
{task.type === "recurring"
|
|
368
|
-
? <RecurringIcon className="w-3 h-3" />
|
|
369
|
-
: task.execute_at
|
|
370
|
-
? <ScheduledIcon className="w-3 h-3" />
|
|
371
|
-
: <TaskOnceIcon className="w-3 h-3" />
|
|
372
|
-
}
|
|
373
|
-
{task.type === "recurring" && task.recurrence ? formatCronShort(task.recurrence) : task.type}
|
|
374
|
-
</span>
|
|
375
|
-
{task.next_run && (
|
|
376
|
-
<span className="text-[#f97316]">{formatTaskRelative(task.next_run)}</span>
|
|
377
|
-
)}
|
|
378
|
-
{!task.next_run && task.execute_at && (
|
|
379
|
-
<span className="text-[#f97316]">{formatTaskRelative(task.execute_at)}</span>
|
|
380
298
|
)}
|
|
381
299
|
</div>
|
|
382
300
|
</div>
|
|
383
301
|
);
|
|
384
302
|
}
|
|
385
303
|
|
|
386
|
-
// --- Helpers ---
|
|
387
|
-
|
|
388
|
-
function formatCronShort(cron: string): string {
|
|
389
|
-
try {
|
|
390
|
-
const parts = cron.trim().split(/\s+/);
|
|
391
|
-
if (parts.length !== 5) return cron;
|
|
392
|
-
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
393
|
-
|
|
394
|
-
if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
395
|
-
const n = parseInt(minute.slice(2));
|
|
396
|
-
return n === 1 ? "Every min" : `Every ${n}min`;
|
|
397
|
-
}
|
|
398
|
-
if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
399
|
-
return "Hourly";
|
|
400
|
-
}
|
|
401
|
-
if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
402
|
-
const n = parseInt(hour.slice(2));
|
|
403
|
-
return n === 1 ? "Hourly" : `Every ${n}h`;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const formatTime = (h: string, m: string): string => {
|
|
407
|
-
const hr = parseInt(h);
|
|
408
|
-
const mn = parseInt(m);
|
|
409
|
-
if (isNaN(hr)) return "";
|
|
410
|
-
const ampm = hr >= 12 ? "PM" : "AM";
|
|
411
|
-
const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
|
|
412
|
-
return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
|
|
416
|
-
const timeStr = formatTime(hour, minute);
|
|
417
|
-
if (dayOfWeek === "*") return `Daily ${timeStr}`;
|
|
418
|
-
const days = dayOfWeek.split(",").map(d => TASK_DAY_NAMES[parseInt(d.trim())] || d);
|
|
419
|
-
if (days.length === 1) return `${days[0]} ${timeStr}`;
|
|
420
|
-
return `${days.join(", ")} ${timeStr}`;
|
|
421
|
-
}
|
|
422
|
-
return cron;
|
|
423
|
-
} catch {
|
|
424
|
-
return cron;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function formatTaskRelative(dateStr: string): string {
|
|
429
|
-
const date = new Date(dateStr);
|
|
430
|
-
const now = new Date();
|
|
431
|
-
const diffMs = date.getTime() - now.getTime();
|
|
432
|
-
const isFuture = diffMs > 0;
|
|
433
|
-
const absDiffMs = Math.abs(diffMs);
|
|
434
|
-
const minutes = Math.floor(absDiffMs / 60000);
|
|
435
|
-
const hours = Math.floor(absDiffMs / 3600000);
|
|
436
|
-
const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
437
|
-
const isToday = date.toDateString() === now.toDateString();
|
|
438
|
-
const tomorrow = new Date(now);
|
|
439
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
440
|
-
const isTomorrow = date.toDateString() === tomorrow.toDateString();
|
|
441
|
-
|
|
442
|
-
if (isToday) {
|
|
443
|
-
if (minutes < 1) return "now";
|
|
444
|
-
if (minutes < 60) return isFuture ? `in ${minutes}m` : `${minutes}m ago`;
|
|
445
|
-
return isFuture ? `in ${hours}h (${timeStr})` : `${hours}h ago`;
|
|
446
|
-
}
|
|
447
|
-
if (isTomorrow) return `Tomorrow ${timeStr}`;
|
|
448
|
-
return `${TASK_DAY_NAMES[date.getDay()]} ${timeStr}`;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
304
|
function timeAgo(timestamp: string): string {
|
|
452
305
|
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
453
306
|
if (seconds < 5) return "just now";
|