apteva 0.4.15 → 0.4.17
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.fq4xbpcz.js +228 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +10 -0
- package/src/db.ts +159 -4
- package/src/mcp-handler.ts +387 -0
- package/src/routes/api/agent-utils.ts +12 -2
- package/src/routes/api/mcp.ts +174 -3
- package/src/server.ts +3 -3
- package/src/web/App.tsx +11 -2
- package/src/web/components/activity/ActivityPage.tsx +326 -0
- package/src/web/components/activity/index.ts +1 -0
- package/src/web/components/common/Icons.tsx +35 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +81 -1
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/McpPage.tsx +9 -3
- package/src/web/components/tasks/TasksPage.tsx +122 -15
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/src/web/types.ts +1 -1
- package/dist/App.jdzxkzm1.js +0 -228
package/src/web/App.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import type { Agent, Provider, Route, NewAgentForm } from "./types";
|
|
|
7
7
|
import { DEFAULT_FEATURES } from "./types";
|
|
8
8
|
|
|
9
9
|
// Context
|
|
10
|
-
import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects, useAgentStatusChange } from "./context";
|
|
10
|
+
import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects, useAgentStatusChange, useTaskChange } from "./context";
|
|
11
11
|
|
|
12
12
|
// Hooks
|
|
13
13
|
import { useAgents, useProviders, useOnboarding } from "./hooks";
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
CreateAgentModal,
|
|
24
24
|
AgentsView,
|
|
25
25
|
Dashboard,
|
|
26
|
+
ActivityPage,
|
|
26
27
|
TasksPage,
|
|
27
28
|
McpPage,
|
|
28
29
|
SkillsPage,
|
|
@@ -38,6 +39,7 @@ function AppContent() {
|
|
|
38
39
|
const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
|
|
39
40
|
const { currentProjectId, refreshProjects } = useProjects();
|
|
40
41
|
const statusChangeCounter = useAgentStatusChange();
|
|
42
|
+
const taskChangeCounter = useTaskChange();
|
|
41
43
|
|
|
42
44
|
// Onboarding state
|
|
43
45
|
const { isComplete: onboardingComplete, setIsComplete: setOnboardingComplete } = useOnboarding();
|
|
@@ -106,7 +108,7 @@ function AppContent() {
|
|
|
106
108
|
};
|
|
107
109
|
|
|
108
110
|
fetchTaskCount();
|
|
109
|
-
}, [shouldFetchData, accessToken, currentProjectId, agents, statusChangeCounter]);
|
|
111
|
+
}, [shouldFetchData, accessToken, currentProjectId, agents, statusChangeCounter, taskChangeCounter]);
|
|
110
112
|
|
|
111
113
|
// Form state
|
|
112
114
|
const [newAgent, setNewAgent] = useState<NewAgentForm>({
|
|
@@ -254,6 +256,13 @@ function AppContent() {
|
|
|
254
256
|
<main className="flex-1 overflow-hidden flex">
|
|
255
257
|
{route === "settings" && <SettingsPage />}
|
|
256
258
|
|
|
259
|
+
{route === "activity" && (
|
|
260
|
+
<ActivityPage
|
|
261
|
+
agents={agents}
|
|
262
|
+
loading={loading}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
|
|
257
266
|
{route === "agents" && (
|
|
258
267
|
<AgentsView
|
|
259
268
|
agents={agents}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect, useRef } from "react";
|
|
2
|
+
import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
|
|
3
|
+
import type { TelemetryEvent } from "../../context";
|
|
4
|
+
import type { Agent } from "../../types";
|
|
5
|
+
import { CloseIcon } from "../common/Icons";
|
|
6
|
+
|
|
7
|
+
interface ActivityPageProps {
|
|
8
|
+
agents: Agent[];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ActivityPage({ agents, loading }: ActivityPageProps) {
|
|
13
|
+
const { currentProjectId } = useProjects();
|
|
14
|
+
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const filteredAgents = useMemo(() => {
|
|
17
|
+
if (currentProjectId === null) return agents;
|
|
18
|
+
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
19
|
+
return agents.filter(a => a.projectId === currentProjectId);
|
|
20
|
+
}, [agents, currentProjectId]);
|
|
21
|
+
|
|
22
|
+
const selectedAgent = filteredAgents.find(a => a.id === selectedAgentId) || null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
26
|
+
{/* Top: Agent Visualization */}
|
|
27
|
+
<div className="flex-[3] min-h-0 p-6 overflow-auto">
|
|
28
|
+
<ActivityVisualization
|
|
29
|
+
agents={filteredAgents}
|
|
30
|
+
loading={loading}
|
|
31
|
+
selectedAgentId={selectedAgentId}
|
|
32
|
+
onSelectAgent={setSelectedAgentId}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Bottom: Command + Stream */}
|
|
37
|
+
<div className="flex-[2] min-h-0 border-t border-[#1a1a1a] flex">
|
|
38
|
+
<QuickCommandPanel
|
|
39
|
+
agent={selectedAgent}
|
|
40
|
+
onClose={() => setSelectedAgentId(null)}
|
|
41
|
+
/>
|
|
42
|
+
<LiveActivityStream agents={filteredAgents} />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Visualization Grid ---
|
|
49
|
+
|
|
50
|
+
function ActivityVisualization({ agents, loading, selectedAgentId, onSelectAgent }: {
|
|
51
|
+
agents: Agent[];
|
|
52
|
+
loading: boolean;
|
|
53
|
+
selectedAgentId: string | null;
|
|
54
|
+
onSelectAgent: (id: string | null) => void;
|
|
55
|
+
}) {
|
|
56
|
+
if (loading) {
|
|
57
|
+
return <div className="flex items-center justify-center h-full text-[#666]">Loading agents...</div>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (agents.length === 0) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex items-center justify-center h-full text-[#666]">
|
|
63
|
+
<div className="text-center">
|
|
64
|
+
<p className="text-lg">No agents found</p>
|
|
65
|
+
<p className="text-sm text-[#444] mt-1">Create and start agents to see them here</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const runningCount = agents.filter(a => a.status === "running").length;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="h-full flex flex-col">
|
|
75
|
+
<div className="mb-4 flex items-center justify-between">
|
|
76
|
+
<h2 className="text-lg font-semibold">Activity</h2>
|
|
77
|
+
<span className="text-sm text-[#666]">
|
|
78
|
+
{runningCount} of {agents.length} running
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex-1 flex items-center justify-center">
|
|
82
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-8">
|
|
83
|
+
{agents.map(agent => (
|
|
84
|
+
<AgentNode
|
|
85
|
+
key={agent.id}
|
|
86
|
+
agent={agent}
|
|
87
|
+
selected={selectedAgentId === agent.id}
|
|
88
|
+
onClick={() => onSelectAgent(selectedAgentId === agent.id ? null : agent.id)}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Agent Node ---
|
|
98
|
+
|
|
99
|
+
function AgentNode({ agent, selected, onClick }: {
|
|
100
|
+
agent: Agent;
|
|
101
|
+
selected: boolean;
|
|
102
|
+
onClick: () => void;
|
|
103
|
+
}) {
|
|
104
|
+
const { isActive, type } = useAgentActivity(agent.id);
|
|
105
|
+
const isRunning = agent.status === "running";
|
|
106
|
+
|
|
107
|
+
const ringStyle = selected
|
|
108
|
+
? "ring-2 ring-[#f97316] shadow-[0_0_12px_rgba(249,115,22,0.3)]"
|
|
109
|
+
: isRunning && isActive
|
|
110
|
+
? "ring-2 ring-green-400"
|
|
111
|
+
: isRunning
|
|
112
|
+
? "ring-1 ring-[#3b82f6]/60"
|
|
113
|
+
: "ring-1 ring-[#333]";
|
|
114
|
+
|
|
115
|
+
const bgClass = isRunning
|
|
116
|
+
? isActive ? "bg-green-500/10" : "bg-[#1a1a1a]"
|
|
117
|
+
: "bg-[#111]";
|
|
118
|
+
|
|
119
|
+
const textClass = isRunning ? "text-[#e0e0e0]" : "text-[#555]";
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<button
|
|
123
|
+
onClick={onClick}
|
|
124
|
+
className="flex flex-col items-center gap-2 group"
|
|
125
|
+
>
|
|
126
|
+
<div className={`w-16 h-16 rounded-full ${bgClass} ${ringStyle} flex items-center justify-center transition-all duration-300 group-hover:scale-110 relative`}>
|
|
127
|
+
<span className={`text-xl font-semibold ${textClass}`}>
|
|
128
|
+
{agent.name.charAt(0).toUpperCase()}
|
|
129
|
+
</span>
|
|
130
|
+
{isActive && isRunning && (
|
|
131
|
+
<div className="absolute inset-0 rounded-full bg-green-400/20 animate-ping" style={{ animationDuration: "1.5s" }} />
|
|
132
|
+
)}
|
|
133
|
+
{isRunning && isActive && (
|
|
134
|
+
<div className="absolute inset-0 rounded-full animate-pulse" style={{ boxShadow: "0 0 12px 3px rgba(74, 222, 128, 0.4)" }} />
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="text-center max-w-[100px]">
|
|
138
|
+
<p className={`text-xs font-medium truncate ${textClass}`}>{agent.name}</p>
|
|
139
|
+
{isActive && type ? (
|
|
140
|
+
<p className="text-[10px] text-green-400 truncate">{type}</p>
|
|
141
|
+
) : (
|
|
142
|
+
<p className={`text-[10px] ${isRunning ? "text-[#3b82f6]" : "text-[#444]"}`}>
|
|
143
|
+
{isRunning ? "idle" : "stopped"}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Live Activity Stream ---
|
|
152
|
+
|
|
153
|
+
const categoryColors: Record<string, string> = {
|
|
154
|
+
LLM: "bg-purple-500/20 text-purple-400",
|
|
155
|
+
TOOL: "bg-blue-500/20 text-blue-400",
|
|
156
|
+
CHAT: "bg-green-500/20 text-green-400",
|
|
157
|
+
ERROR: "bg-red-500/20 text-red-400",
|
|
158
|
+
SYSTEM: "bg-gray-500/20 text-gray-400",
|
|
159
|
+
TASK: "bg-yellow-500/20 text-yellow-400",
|
|
160
|
+
MEMORY: "bg-cyan-500/20 text-cyan-400",
|
|
161
|
+
MCP: "bg-orange-500/20 text-orange-400",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function LiveActivityStream({ agents }: { agents: Agent[] }) {
|
|
165
|
+
const { events } = useTelemetryContext();
|
|
166
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
|
|
168
|
+
const agentIds = useMemo(() => new Set(agents.map(a => a.id)), [agents]);
|
|
169
|
+
const agentNameMap = useMemo(() => {
|
|
170
|
+
const map = new Map<string, string>();
|
|
171
|
+
agents.forEach(a => map.set(a.id, a.name));
|
|
172
|
+
return map;
|
|
173
|
+
}, [agents]);
|
|
174
|
+
|
|
175
|
+
const filteredEvents = useMemo(() => {
|
|
176
|
+
return events
|
|
177
|
+
.filter(e => agentIds.has(e.agent_id))
|
|
178
|
+
.slice(0, 50);
|
|
179
|
+
}, [events, agentIds]);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex-1 flex flex-col overflow-hidden border-l border-[#1a1a1a]">
|
|
183
|
+
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
184
|
+
<h3 className="font-semibold text-sm">Live Activity</h3>
|
|
185
|
+
<span className="text-xs text-[#666]">{filteredEvents.length} events</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div ref={scrollRef} className="flex-1 overflow-auto">
|
|
188
|
+
{filteredEvents.length === 0 ? (
|
|
189
|
+
<div className="p-4 text-center text-[#666] text-sm">
|
|
190
|
+
No activity yet. Events appear in real-time.
|
|
191
|
+
</div>
|
|
192
|
+
) : (
|
|
193
|
+
<div className="divide-y divide-[#1a1a1a]">
|
|
194
|
+
{filteredEvents.map(event => (
|
|
195
|
+
<div key={event.id} className="px-4 py-2 hover:bg-[#111] transition" style={{ animation: "slideIn 0.3s ease-out" }}>
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${categoryColors[event.category] || "bg-[#222] text-[#888]"}`}>
|
|
198
|
+
{event.category}
|
|
199
|
+
</span>
|
|
200
|
+
<span className="text-xs font-medium truncate flex-1">{event.type}</span>
|
|
201
|
+
<span className="text-[10px] text-[#555] shrink-0">{timeAgo(event.timestamp)}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="text-[10px] text-[#555] mt-0.5">
|
|
204
|
+
{agentNameMap.get(event.agent_id) || event.agent_id}
|
|
205
|
+
{event.duration_ms ? ` \u00b7 ${event.duration_ms}ms` : ""}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Quick Command Panel ---
|
|
217
|
+
|
|
218
|
+
function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: () => void }) {
|
|
219
|
+
const { authFetch } = useAuth();
|
|
220
|
+
const [command, setCommand] = useState("");
|
|
221
|
+
const [sending, setSending] = useState(false);
|
|
222
|
+
const [toast, setToast] = useState<string | null>(null);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
setCommand("");
|
|
226
|
+
setToast(null);
|
|
227
|
+
}, [agent?.id]);
|
|
228
|
+
|
|
229
|
+
if (!agent) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="w-80 shrink-0 flex items-center justify-center text-[#555] text-sm p-4 text-center">
|
|
232
|
+
Click an agent to send a quick command
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const handleSend = async () => {
|
|
238
|
+
if (!command.trim() || sending) return;
|
|
239
|
+
if (agent.status !== "running") {
|
|
240
|
+
setToast("Agent is not running");
|
|
241
|
+
setTimeout(() => setToast(null), 3000);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
setSending(true);
|
|
245
|
+
try {
|
|
246
|
+
const res = await authFetch(`/api/agents/${agent.id}/chat`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body: JSON.stringify({ message: command, agent_id: agent.id }),
|
|
250
|
+
});
|
|
251
|
+
if (res.ok) {
|
|
252
|
+
setToast("Command sent");
|
|
253
|
+
setCommand("");
|
|
254
|
+
} else {
|
|
255
|
+
const data = await res.json().catch(() => ({}));
|
|
256
|
+
setToast(data.error || "Failed to send");
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
setToast("Failed to send command");
|
|
260
|
+
} finally {
|
|
261
|
+
setSending(false);
|
|
262
|
+
setTimeout(() => setToast(null), 3000);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const isRunning = agent.status === "running";
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div className="w-80 shrink-0 flex flex-col">
|
|
270
|
+
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
271
|
+
<div className="min-w-0">
|
|
272
|
+
<h3 className="font-semibold text-sm truncate">{agent.name}</h3>
|
|
273
|
+
<p className={`text-[10px] ${isRunning ? "text-green-400" : "text-[#666]"}`}>
|
|
274
|
+
{isRunning ? "Running" : "Stopped"}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<button onClick={onClose} className="text-[#666] hover:text-[#e0e0e0] transition shrink-0 ml-2">
|
|
278
|
+
<CloseIcon className="w-4 h-4" />
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="flex-1 p-4 flex flex-col justify-end">
|
|
282
|
+
{toast && (
|
|
283
|
+
<div className={`mb-3 px-3 py-2 rounded text-xs ${
|
|
284
|
+
toast === "Command sent"
|
|
285
|
+
? "bg-green-500/10 border border-green-500/20 text-green-400"
|
|
286
|
+
: "bg-red-500/10 border border-red-500/20 text-red-400"
|
|
287
|
+
}`}>
|
|
288
|
+
{toast}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
<div className="flex gap-2">
|
|
292
|
+
<input
|
|
293
|
+
type="text"
|
|
294
|
+
value={command}
|
|
295
|
+
onChange={e => setCommand(e.target.value)}
|
|
296
|
+
onKeyDown={e => e.key === "Enter" && handleSend()}
|
|
297
|
+
placeholder={isRunning ? "Quick command..." : "Agent not running"}
|
|
298
|
+
disabled={sending || !isRunning}
|
|
299
|
+
className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
|
|
300
|
+
/>
|
|
301
|
+
<button
|
|
302
|
+
onClick={handleSend}
|
|
303
|
+
disabled={sending || !command.trim() || !isRunning}
|
|
304
|
+
className="px-3 py-2 bg-[#f97316]/20 text-[#f97316] rounded text-sm font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
|
|
305
|
+
>
|
|
306
|
+
{sending ? "..." : "Send"}
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- Helpers ---
|
|
315
|
+
|
|
316
|
+
function timeAgo(timestamp: string): string {
|
|
317
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
318
|
+
if (seconds < 5) return "just now";
|
|
319
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
320
|
+
const minutes = Math.floor(seconds / 60);
|
|
321
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
322
|
+
const hours = Math.floor(minutes / 60);
|
|
323
|
+
if (hours < 24) return `${hours}h ago`;
|
|
324
|
+
const days = Math.floor(hours / 24);
|
|
325
|
+
return `${days}d ago`;
|
|
326
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ActivityPage } from "./ActivityPage";
|
|
@@ -182,6 +182,14 @@ export function TestsIcon({ className = "w-4 h-4" }: IconProps) {
|
|
|
182
182
|
);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
export function ActivityIcon({ className = "w-5 h-5" }: IconProps) {
|
|
186
|
+
return (
|
|
187
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
188
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
189
|
+
</svg>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
185
193
|
export function PlusIcon({ className = "w-4 h-4" }: IconProps) {
|
|
186
194
|
return (
|
|
187
195
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -189,3 +197,30 @@ export function PlusIcon({ className = "w-4 h-4" }: IconProps) {
|
|
|
189
197
|
</svg>
|
|
190
198
|
);
|
|
191
199
|
}
|
|
200
|
+
|
|
201
|
+
export function RecurringIcon({ className = "w-4 h-4" }: IconProps) {
|
|
202
|
+
return (
|
|
203
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
204
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h5M20 20v-5h-5" />
|
|
205
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.49 9A9 9 0 005.64 5.64L4 4m16 16l-1.64-1.64A9 9 0 013.51 15" />
|
|
206
|
+
</svg>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function ScheduledIcon({ className = "w-4 h-4" }: IconProps) {
|
|
211
|
+
return (
|
|
212
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
213
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3" />
|
|
214
|
+
<circle cx="12" cy="12" r="9" strokeWidth={2} />
|
|
215
|
+
</svg>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function TaskOnceIcon({ className = "w-4 h-4" }: IconProps) {
|
|
220
|
+
return (
|
|
221
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
222
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
223
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
|
|
224
|
+
</svg>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -198,7 +198,18 @@ export function Dashboard({
|
|
|
198
198
|
>
|
|
199
199
|
<div className="flex-1 min-w-0">
|
|
200
200
|
<p className="font-medium truncate">{task.title}</p>
|
|
201
|
-
<p className="text-sm text-[#666]">
|
|
201
|
+
<p className="text-sm text-[#666]">
|
|
202
|
+
{task.agentName}
|
|
203
|
+
{task.recurrence && (
|
|
204
|
+
<span className="ml-1 text-[#555]">· {formatCronShort(task.recurrence)}</span>
|
|
205
|
+
)}
|
|
206
|
+
{task.next_run && (
|
|
207
|
+
<span className="ml-1 text-[#f97316]">· {formatRelativeShort(task.next_run)}</span>
|
|
208
|
+
)}
|
|
209
|
+
{!task.next_run && task.execute_at && (
|
|
210
|
+
<span className="ml-1 text-[#f97316]">· {formatRelativeShort(task.execute_at)}</span>
|
|
211
|
+
)}
|
|
212
|
+
</p>
|
|
202
213
|
</div>
|
|
203
214
|
<TaskStatusBadge status={task.status} />
|
|
204
215
|
</div>
|
|
@@ -330,3 +341,72 @@ function TaskStatusBadge({ status }: { status: Task["status"] }) {
|
|
|
330
341
|
</span>
|
|
331
342
|
);
|
|
332
343
|
}
|
|
344
|
+
|
|
345
|
+
// --- Schedule formatting helpers (compact versions for dashboard) ---
|
|
346
|
+
|
|
347
|
+
const DASH_DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
348
|
+
|
|
349
|
+
function formatCronShort(cron: string): string {
|
|
350
|
+
try {
|
|
351
|
+
const parts = cron.trim().split(/\s+/);
|
|
352
|
+
if (parts.length !== 5) return cron;
|
|
353
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
354
|
+
|
|
355
|
+
if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
356
|
+
const n = parseInt(minute.slice(2));
|
|
357
|
+
return n === 1 ? "Every min" : `Every ${n}min`;
|
|
358
|
+
}
|
|
359
|
+
if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
360
|
+
return "Hourly";
|
|
361
|
+
}
|
|
362
|
+
if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
363
|
+
const n = parseInt(hour.slice(2));
|
|
364
|
+
return n === 1 ? "Hourly" : `Every ${n}h`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const formatTime = (h: string, m: string): string => {
|
|
368
|
+
const hr = parseInt(h);
|
|
369
|
+
const mn = parseInt(m);
|
|
370
|
+
if (isNaN(hr)) return "";
|
|
371
|
+
const ampm = hr >= 12 ? "PM" : "AM";
|
|
372
|
+
const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
|
|
373
|
+
return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
|
|
377
|
+
const timeStr = formatTime(hour, minute);
|
|
378
|
+
if (dayOfWeek === "*") return `Daily ${timeStr}`;
|
|
379
|
+
const days = dayOfWeek.split(",").map(d => DASH_DAY_NAMES[parseInt(d.trim())] || d);
|
|
380
|
+
if (days.length === 1) return `${days[0]} ${timeStr}`;
|
|
381
|
+
return `${days.join(" & ")} ${timeStr}`;
|
|
382
|
+
}
|
|
383
|
+
return cron;
|
|
384
|
+
} catch {
|
|
385
|
+
return cron;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function formatRelativeShort(dateStr: string): string {
|
|
390
|
+
const date = new Date(dateStr);
|
|
391
|
+
const now = new Date();
|
|
392
|
+
const diffMs = date.getTime() - now.getTime();
|
|
393
|
+
const isFuture = diffMs > 0;
|
|
394
|
+
const absDiffMs = Math.abs(diffMs);
|
|
395
|
+
const minutes = Math.floor(absDiffMs / 60000);
|
|
396
|
+
const hours = Math.floor(absDiffMs / 3600000);
|
|
397
|
+
|
|
398
|
+
const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
399
|
+
|
|
400
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
401
|
+
const tomorrow = new Date(now);
|
|
402
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
403
|
+
const isTomorrow = date.toDateString() === tomorrow.toDateString();
|
|
404
|
+
|
|
405
|
+
if (isToday) {
|
|
406
|
+
if (minutes < 1) return "now";
|
|
407
|
+
if (minutes < 60) return isFuture ? `in ${minutes}m` : `${minutes}m ago`;
|
|
408
|
+
return isFuture ? `in ${hours}h` : `${hours}h ago`;
|
|
409
|
+
}
|
|
410
|
+
if (isTomorrow) return `Tomorrow ${timeStr}`;
|
|
411
|
+
return `${DASH_DAY_NAMES[date.getDay()]} ${timeStr}`;
|
|
412
|
+
}
|
|
@@ -12,6 +12,7 @@ export { OnboardingWizard } from "./onboarding";
|
|
|
12
12
|
export { SettingsPage } from "./settings";
|
|
13
13
|
export { AgentCard, CreateAgentModal, AgentPanel, AgentsView } from "./agents";
|
|
14
14
|
export { Dashboard } from "./dashboard";
|
|
15
|
+
export { ActivityPage } from "./activity";
|
|
15
16
|
export { TasksPage } from "./tasks";
|
|
16
17
|
export { McpPage } from "./mcp";
|
|
17
18
|
export { SkillsPage } from "./skills/SkillsPage";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, SkillsIcon, TestsIcon, TelemetryIcon, ApiIcon, SettingsIcon, CloseIcon } from "../common/Icons";
|
|
2
|
+
import { DashboardIcon, ActivityIcon, AgentsIcon, TasksIcon, McpIcon, SkillsIcon, TestsIcon, TelemetryIcon, ApiIcon, SettingsIcon, CloseIcon } from "../common/Icons";
|
|
3
3
|
import type { Route } from "../../types";
|
|
4
4
|
|
|
5
5
|
interface SidebarProps {
|
|
@@ -63,6 +63,12 @@ export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onCl
|
|
|
63
63
|
onClick={() => handleNavigate("agents")}
|
|
64
64
|
badge={agentCount > 0 ? String(agentCount) : undefined}
|
|
65
65
|
/>
|
|
66
|
+
<NavButton
|
|
67
|
+
icon={<ActivityIcon />}
|
|
68
|
+
label="Activity"
|
|
69
|
+
active={route === "activity"}
|
|
70
|
+
onClick={() => handleNavigate("activity")}
|
|
71
|
+
/>
|
|
66
72
|
<NavButton
|
|
67
73
|
icon={<TasksIcon />}
|
|
68
74
|
label="Tasks"
|
|
@@ -948,9 +948,12 @@ function HostedServices({ onServerAdded, projectId }: { onServerAdded?: () => vo
|
|
|
948
948
|
|
|
949
949
|
const fetchStatus = async () => {
|
|
950
950
|
try {
|
|
951
|
+
const serversUrl = projectId && projectId !== "unassigned"
|
|
952
|
+
? `/api/mcp/servers?project=${encodeURIComponent(projectId)}`
|
|
953
|
+
: "/api/mcp/servers";
|
|
951
954
|
const [providersRes, serversRes] = await Promise.all([
|
|
952
955
|
authFetch("/api/providers"),
|
|
953
|
-
authFetch(
|
|
956
|
+
authFetch(serversUrl),
|
|
954
957
|
]);
|
|
955
958
|
const providersData = await providersRes.json();
|
|
956
959
|
const serversData = await serversRes.json();
|
|
@@ -1037,7 +1040,7 @@ function HostedServices({ onServerAdded, projectId }: { onServerAdded?: () => vo
|
|
|
1037
1040
|
|
|
1038
1041
|
useEffect(() => {
|
|
1039
1042
|
fetchStatus();
|
|
1040
|
-
}, [authFetch]);
|
|
1043
|
+
}, [authFetch, projectId]);
|
|
1041
1044
|
|
|
1042
1045
|
if (loading) {
|
|
1043
1046
|
return <div className="text-center py-8 text-[#666]">Loading...</div>;
|
|
@@ -1360,9 +1363,12 @@ function AgentDojoContent({
|
|
|
1360
1363
|
setLoadingConfigs(true);
|
|
1361
1364
|
try {
|
|
1362
1365
|
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
1366
|
+
const serversUrl = projectId && projectId !== "unassigned"
|
|
1367
|
+
? `/api/mcp/servers?project=${encodeURIComponent(projectId)}`
|
|
1368
|
+
: "/api/mcp/servers";
|
|
1363
1369
|
const [configsRes, serversRes] = await Promise.all([
|
|
1364
1370
|
authFetch(`/api/integrations/agentdojo/configs${projectParam}`),
|
|
1365
|
-
authFetch(
|
|
1371
|
+
authFetch(serversUrl),
|
|
1366
1372
|
]);
|
|
1367
1373
|
const configsData = await configsRes.json();
|
|
1368
1374
|
const serversData = await serversRes.json();
|