apteva 0.2.7 → 0.2.8
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.hzbfeg94.js +217 -0
- package/dist/index.html +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +561 -32
- package/src/routes/api.ts +901 -35
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +46 -5
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- package/src/web/components/agents/AgentPanel.tsx +751 -11
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +48 -0
- package/src/web/components/common/Modal.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- package/src/web/components/layout/Sidebar.tsx +81 -43
- package/src/web/components/mcp/McpPage.tsx +261 -32
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +320 -21
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +6 -0
- package/dist/App.3kb50qa3.js +0 -213
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { UserDB } from "../db";
|
|
2
|
+
import {
|
|
3
|
+
login,
|
|
4
|
+
refreshSession,
|
|
5
|
+
invalidateSession,
|
|
6
|
+
getAuthStatus,
|
|
7
|
+
verifyAccessToken,
|
|
8
|
+
hashPassword,
|
|
9
|
+
validatePassword,
|
|
10
|
+
REFRESH_TOKEN_EXPIRY,
|
|
11
|
+
} from "../auth";
|
|
12
|
+
import {
|
|
13
|
+
getTokenFromRequest,
|
|
14
|
+
getRefreshTokenFromCookie,
|
|
15
|
+
createRefreshTokenCookie,
|
|
16
|
+
clearRefreshTokenCookie,
|
|
17
|
+
} from "../auth/middleware";
|
|
18
|
+
|
|
19
|
+
function json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
20
|
+
return new Response(JSON.stringify(data), {
|
|
21
|
+
status,
|
|
22
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handleAuthRequest(req: Request, path: string): Promise<Response> {
|
|
27
|
+
const method = req.method;
|
|
28
|
+
|
|
29
|
+
// GET /api/auth/check - Check authentication status
|
|
30
|
+
if (path === "/api/auth/check" && method === "GET") {
|
|
31
|
+
const token = getTokenFromRequest(req);
|
|
32
|
+
const status = getAuthStatus(token || undefined);
|
|
33
|
+
return json(status);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// POST /api/auth/login - Login with username and password
|
|
37
|
+
if (path === "/api/auth/login" && method === "POST") {
|
|
38
|
+
try {
|
|
39
|
+
const body = await req.json();
|
|
40
|
+
const { username, password } = body;
|
|
41
|
+
|
|
42
|
+
if (!username || !password) {
|
|
43
|
+
return json({ error: "Username and password are required" }, 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await login(username, password);
|
|
47
|
+
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
return json({ error: result.error }, 401);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Set refresh token as httpOnly cookie
|
|
53
|
+
const cookieHeader = createRefreshTokenCookie(result.tokens!.refreshToken, REFRESH_TOKEN_EXPIRY);
|
|
54
|
+
|
|
55
|
+
return json(
|
|
56
|
+
{
|
|
57
|
+
user: result.user,
|
|
58
|
+
accessToken: result.tokens!.accessToken,
|
|
59
|
+
expiresIn: result.tokens!.expiresIn,
|
|
60
|
+
},
|
|
61
|
+
200,
|
|
62
|
+
{ "Set-Cookie": cookieHeader }
|
|
63
|
+
);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return json({ error: "Invalid request body" }, 400);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// POST /api/auth/logout - Logout (invalidate refresh token)
|
|
70
|
+
if (path === "/api/auth/logout" && method === "POST") {
|
|
71
|
+
const refreshToken = getRefreshTokenFromCookie(req);
|
|
72
|
+
|
|
73
|
+
if (refreshToken) {
|
|
74
|
+
invalidateSession(refreshToken);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Clear the cookie
|
|
78
|
+
return json(
|
|
79
|
+
{ success: true },
|
|
80
|
+
200,
|
|
81
|
+
{ "Set-Cookie": clearRefreshTokenCookie() }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// POST /api/auth/refresh - Refresh access token
|
|
86
|
+
if (path === "/api/auth/refresh" && method === "POST") {
|
|
87
|
+
// No users = no valid sessions possible
|
|
88
|
+
if (!UserDB.hasUsers()) {
|
|
89
|
+
return json({ error: "No users exist" }, 401, { "Set-Cookie": clearRefreshTokenCookie() });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const refreshToken = getRefreshTokenFromCookie(req);
|
|
93
|
+
|
|
94
|
+
if (!refreshToken) {
|
|
95
|
+
return json({ error: "No refresh token" }, 401);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await refreshSession(refreshToken);
|
|
99
|
+
|
|
100
|
+
if (!result) {
|
|
101
|
+
return json(
|
|
102
|
+
{ error: "Invalid or expired refresh token" },
|
|
103
|
+
401,
|
|
104
|
+
{ "Set-Cookie": clearRefreshTokenCookie() }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set new refresh token cookie
|
|
109
|
+
const cookieHeader = createRefreshTokenCookie(result.refreshToken, REFRESH_TOKEN_EXPIRY);
|
|
110
|
+
|
|
111
|
+
return json(
|
|
112
|
+
{
|
|
113
|
+
accessToken: result.accessToken,
|
|
114
|
+
expiresIn: result.expiresIn,
|
|
115
|
+
},
|
|
116
|
+
200,
|
|
117
|
+
{ "Set-Cookie": cookieHeader }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// GET /api/auth/me - Get current user
|
|
122
|
+
if (path === "/api/auth/me" && method === "GET") {
|
|
123
|
+
const token = getTokenFromRequest(req);
|
|
124
|
+
|
|
125
|
+
if (!token) {
|
|
126
|
+
return json({ error: "Unauthorized" }, 401);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const payload = verifyAccessToken(token);
|
|
130
|
+
if (!payload) {
|
|
131
|
+
return json({ error: "Invalid or expired token" }, 401);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const user = UserDB.findById(payload.userId);
|
|
135
|
+
if (!user) {
|
|
136
|
+
return json({ error: "User not found" }, 404);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return json({
|
|
140
|
+
user: {
|
|
141
|
+
id: user.id,
|
|
142
|
+
username: user.username,
|
|
143
|
+
email: user.email,
|
|
144
|
+
role: user.role,
|
|
145
|
+
createdAt: user.created_at,
|
|
146
|
+
lastLoginAt: user.last_login_at,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// PUT /api/auth/me - Update current user profile
|
|
152
|
+
if (path === "/api/auth/me" && method === "PUT") {
|
|
153
|
+
const token = getTokenFromRequest(req);
|
|
154
|
+
|
|
155
|
+
if (!token) {
|
|
156
|
+
return json({ error: "Unauthorized" }, 401);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const payload = verifyAccessToken(token);
|
|
160
|
+
if (!payload) {
|
|
161
|
+
return json({ error: "Invalid or expired token" }, 401);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const user = UserDB.findById(payload.userId);
|
|
165
|
+
if (!user) {
|
|
166
|
+
return json({ error: "User not found" }, 404);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const body = await req.json();
|
|
171
|
+
const updates: Parameters<typeof UserDB.update>[1] = {};
|
|
172
|
+
|
|
173
|
+
if (body.email !== undefined) updates.email = body.email;
|
|
174
|
+
|
|
175
|
+
const updated = UserDB.update(user.id, updates);
|
|
176
|
+
|
|
177
|
+
return json({
|
|
178
|
+
user: updated ? {
|
|
179
|
+
id: updated.id,
|
|
180
|
+
username: updated.username,
|
|
181
|
+
email: updated.email,
|
|
182
|
+
role: updated.role,
|
|
183
|
+
createdAt: updated.created_at,
|
|
184
|
+
lastLoginAt: updated.last_login_at,
|
|
185
|
+
} : null,
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return json({ error: "Invalid request body" }, 400);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// PUT /api/auth/password - Change password
|
|
193
|
+
if (path === "/api/auth/password" && method === "PUT") {
|
|
194
|
+
const token = getTokenFromRequest(req);
|
|
195
|
+
|
|
196
|
+
if (!token) {
|
|
197
|
+
return json({ error: "Unauthorized" }, 401);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const payload = verifyAccessToken(token);
|
|
201
|
+
if (!payload) {
|
|
202
|
+
return json({ error: "Invalid or expired token" }, 401);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const user = UserDB.findById(payload.userId);
|
|
206
|
+
if (!user) {
|
|
207
|
+
return json({ error: "User not found" }, 404);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const body = await req.json();
|
|
212
|
+
const { currentPassword, newPassword } = body;
|
|
213
|
+
|
|
214
|
+
if (!currentPassword || !newPassword) {
|
|
215
|
+
return json({ error: "Current and new password are required" }, 400);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Verify current password
|
|
219
|
+
const { verifyPassword } = await import("../auth");
|
|
220
|
+
const isValid = await verifyPassword(currentPassword, user.password_hash);
|
|
221
|
+
if (!isValid) {
|
|
222
|
+
return json({ error: "Current password is incorrect" }, 401);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Validate new password
|
|
226
|
+
const validation = validatePassword(newPassword);
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
return json({ error: validation.errors.join(". ") }, 400);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Update password
|
|
232
|
+
const newHash = await hashPassword(newPassword);
|
|
233
|
+
UserDB.update(user.id, { password_hash: newHash });
|
|
234
|
+
|
|
235
|
+
return json({ success: true, message: "Password updated successfully" });
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return json({ error: "Invalid request body" }, 400);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return json({ error: "Not found" }, 404);
|
|
242
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Server, type Subprocess } from "bun";
|
|
2
2
|
import { handleApiRequest } from "./routes/api";
|
|
3
|
+
import { handleAuthRequest } from "./routes/auth";
|
|
3
4
|
import { serveStatic } from "./routes/static";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
import { homedir } from "os";
|
|
6
7
|
import { mkdirSync, existsSync } from "fs";
|
|
7
8
|
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, type McpServer, type Agent } from "./db";
|
|
9
|
+
import { authMiddleware, type AuthContext } from "./auth/middleware";
|
|
8
10
|
import { startMcpProcess } from "./mcp-client";
|
|
9
11
|
import {
|
|
10
12
|
ensureBinary,
|
|
@@ -215,11 +217,23 @@ const server = Bun.serve({
|
|
|
215
217
|
const url = new URL(req.url);
|
|
216
218
|
const path = url.pathname;
|
|
217
219
|
|
|
218
|
-
//
|
|
220
|
+
// Dev mode route logging
|
|
221
|
+
if (process.env.NODE_ENV !== "production" && path.startsWith("/api/")) {
|
|
222
|
+
const params = url.search ? url.search : "";
|
|
223
|
+
console.log(`[${req.method}] ${path}${params}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// CORS headers - configurable origins
|
|
227
|
+
const origin = req.headers.get("Origin") || "";
|
|
228
|
+
const allowedOrigins = process.env.CORS_ORIGINS?.split(",") || [];
|
|
229
|
+
const isLocalhost = origin.includes("localhost") || origin.includes("127.0.0.1");
|
|
230
|
+
const allowOrigin = allowedOrigins.includes(origin) || isLocalhost || allowedOrigins.length === 0 ? origin || "*" : "";
|
|
231
|
+
|
|
219
232
|
const corsHeaders = {
|
|
220
|
-
"Access-Control-Allow-Origin":
|
|
233
|
+
"Access-Control-Allow-Origin": allowOrigin,
|
|
221
234
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
222
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
235
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
236
|
+
"Access-Control-Allow-Credentials": "true",
|
|
223
237
|
};
|
|
224
238
|
|
|
225
239
|
// Handle preflight
|
|
@@ -229,8 +243,35 @@ const server = Bun.serve({
|
|
|
229
243
|
|
|
230
244
|
// API routes
|
|
231
245
|
if (path.startsWith("/api/")) {
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
// Auth routes handled separately (before middleware)
|
|
247
|
+
if (path.startsWith("/api/auth/")) {
|
|
248
|
+
const response = await handleAuthRequest(req, path);
|
|
249
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
250
|
+
response.headers.set(key, value);
|
|
251
|
+
});
|
|
252
|
+
return response;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Health check endpoint (no auth required for Docker health checks)
|
|
256
|
+
if (path === "/api/health") {
|
|
257
|
+
const response = await handleApiRequest(req, path);
|
|
258
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
259
|
+
response.headers.set(key, value);
|
|
260
|
+
});
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Apply auth middleware
|
|
265
|
+
const { response: authResponse, context } = await authMiddleware(req, path);
|
|
266
|
+
if (authResponse) {
|
|
267
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
268
|
+
authResponse.headers.set(key, value);
|
|
269
|
+
});
|
|
270
|
+
return authResponse;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Pass auth context to API handler
|
|
274
|
+
const response = await handleApiRequest(req, path, context);
|
|
234
275
|
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
235
276
|
response.headers.set(key, value);
|
|
236
277
|
});
|
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 } from "./context";
|
|
10
|
+
import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects } from "./context";
|
|
11
11
|
|
|
12
12
|
// Hooks
|
|
13
13
|
import { useAgents, useProviders, useOnboarding } from "./hooks";
|
|
@@ -26,13 +26,25 @@ import {
|
|
|
26
26
|
TasksPage,
|
|
27
27
|
McpPage,
|
|
28
28
|
TelemetryPage,
|
|
29
|
+
LoginPage,
|
|
29
30
|
} from "./components";
|
|
30
31
|
|
|
31
|
-
function
|
|
32
|
+
function AppContent() {
|
|
33
|
+
// Auth state
|
|
34
|
+
const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
|
|
35
|
+
const { refreshProjects } = useProjects();
|
|
36
|
+
|
|
32
37
|
// Onboarding state
|
|
33
38
|
const { isComplete: onboardingComplete, setIsComplete: setOnboardingComplete } = useOnboarding();
|
|
34
39
|
|
|
35
|
-
//
|
|
40
|
+
// Helper to get auth headers
|
|
41
|
+
const getAuthHeaders = (): Record<string, string> => {
|
|
42
|
+
return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Data hooks - only fetch when authenticated and onboarding complete
|
|
46
|
+
const shouldFetchData = isAuthenticated && onboardingComplete === true;
|
|
47
|
+
|
|
36
48
|
const {
|
|
37
49
|
agents,
|
|
38
50
|
loading,
|
|
@@ -42,13 +54,13 @@ function App() {
|
|
|
42
54
|
updateAgent,
|
|
43
55
|
deleteAgent,
|
|
44
56
|
toggleAgent,
|
|
45
|
-
} = useAgents(
|
|
57
|
+
} = useAgents(shouldFetchData);
|
|
46
58
|
|
|
47
59
|
const {
|
|
48
60
|
providers,
|
|
49
61
|
configuredProviders,
|
|
50
62
|
fetchProviders,
|
|
51
|
-
} = useProviders(
|
|
63
|
+
} = useProviders(shouldFetchData);
|
|
52
64
|
|
|
53
65
|
// UI state
|
|
54
66
|
const [showCreate, setShowCreate] = useState(false);
|
|
@@ -56,14 +68,15 @@ function App() {
|
|
|
56
68
|
const [route, setRoute] = useState<Route>("dashboard");
|
|
57
69
|
const [startError, setStartError] = useState<string | null>(null);
|
|
58
70
|
const [taskCount, setTaskCount] = useState(0);
|
|
71
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
59
72
|
|
|
60
73
|
// Fetch task count periodically
|
|
61
74
|
useEffect(() => {
|
|
62
|
-
if (
|
|
75
|
+
if (!shouldFetchData) return;
|
|
63
76
|
|
|
64
77
|
const fetchTaskCount = async () => {
|
|
65
78
|
try {
|
|
66
|
-
const res = await fetch("/api/tasks?status=pending");
|
|
79
|
+
const res = await fetch("/api/tasks?status=pending", { headers: getAuthHeaders() });
|
|
67
80
|
if (res.ok) {
|
|
68
81
|
const data = await res.json();
|
|
69
82
|
setTaskCount(data.count || 0);
|
|
@@ -76,7 +89,7 @@ function App() {
|
|
|
76
89
|
fetchTaskCount();
|
|
77
90
|
const interval = setInterval(fetchTaskCount, 15000);
|
|
78
91
|
return () => clearInterval(interval);
|
|
79
|
-
}, [
|
|
92
|
+
}, [shouldFetchData, accessToken]);
|
|
80
93
|
|
|
81
94
|
// Form state
|
|
82
95
|
const [newAgent, setNewAgent] = useState<NewAgentForm>({
|
|
@@ -125,6 +138,7 @@ function App() {
|
|
|
125
138
|
const handleCreateAgent = async () => {
|
|
126
139
|
if (!newAgent.name) return;
|
|
127
140
|
await createAgent(newAgent);
|
|
141
|
+
await refreshProjects(); // Refresh project agent counts
|
|
128
142
|
const defaultProvider = configuredProviders[0];
|
|
129
143
|
const defaultModel = defaultProvider?.models.find(m => m.recommended)?.value || defaultProvider?.models[0]?.value || "";
|
|
130
144
|
setNewAgent({
|
|
@@ -152,6 +166,7 @@ function App() {
|
|
|
152
166
|
setSelectedAgent(null);
|
|
153
167
|
}
|
|
154
168
|
await deleteAgent(id);
|
|
169
|
+
await refreshProjects(); // Refresh project agent counts
|
|
155
170
|
};
|
|
156
171
|
|
|
157
172
|
const handleSelectAgent = (agent: Agent) => {
|
|
@@ -168,24 +183,38 @@ function App() {
|
|
|
168
183
|
const handleOnboardingComplete = () => {
|
|
169
184
|
setOnboardingComplete(true);
|
|
170
185
|
fetchProviders();
|
|
186
|
+
// Refresh auth to pick up new state
|
|
187
|
+
checkAuth();
|
|
171
188
|
};
|
|
172
189
|
|
|
190
|
+
// Show loading while checking auth
|
|
191
|
+
if (authLoading || hasUsers === null) {
|
|
192
|
+
return <LoadingSpinner fullScreen />;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// No users exist - show onboarding with account creation
|
|
196
|
+
if (!hasUsers) {
|
|
197
|
+
return <OnboardingWizard onComplete={handleOnboardingComplete} needsAccount={true} />;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Users exist but not authenticated - show login
|
|
201
|
+
if (!isAuthenticated) {
|
|
202
|
+
return <LoginPage />;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
// Show loading while checking onboarding
|
|
174
206
|
if (onboardingComplete === null) {
|
|
175
207
|
return <LoadingSpinner fullScreen />;
|
|
176
208
|
}
|
|
177
209
|
|
|
178
|
-
// Show onboarding if not complete
|
|
210
|
+
// Show onboarding if not complete (but already has account)
|
|
179
211
|
if (!onboardingComplete) {
|
|
180
|
-
return <OnboardingWizard onComplete={handleOnboardingComplete} />;
|
|
212
|
+
return <OnboardingWizard onComplete={handleOnboardingComplete} needsAccount={false} />;
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
return (
|
|
184
216
|
<div className="h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col overflow-hidden">
|
|
185
|
-
<Header
|
|
186
|
-
onNewAgent={() => setShowCreate(true)}
|
|
187
|
-
canCreateAgent={configuredProviders.length > 0}
|
|
188
|
-
/>
|
|
217
|
+
<Header onMenuClick={() => setMobileMenuOpen(true)} />
|
|
189
218
|
|
|
190
219
|
{startError && (
|
|
191
220
|
<ErrorBanner message={startError} onDismiss={() => setStartError(null)} />
|
|
@@ -197,6 +226,8 @@ function App() {
|
|
|
197
226
|
agentCount={agents.length}
|
|
198
227
|
taskCount={taskCount}
|
|
199
228
|
onNavigate={handleNavigate}
|
|
229
|
+
isOpen={mobileMenuOpen}
|
|
230
|
+
onClose={() => setMobileMenuOpen(false)}
|
|
200
231
|
/>
|
|
201
232
|
|
|
202
233
|
<main className="flex-1 overflow-hidden flex">
|
|
@@ -213,6 +244,8 @@ function App() {
|
|
|
213
244
|
onToggleAgent={handleToggleAgent}
|
|
214
245
|
onDeleteAgent={handleDeleteAgent}
|
|
215
246
|
onUpdateAgent={updateAgent}
|
|
247
|
+
onNewAgent={() => setShowCreate(true)}
|
|
248
|
+
canCreateAgent={configuredProviders.length > 0}
|
|
216
249
|
/>
|
|
217
250
|
)}
|
|
218
251
|
|
|
@@ -254,10 +287,19 @@ function App() {
|
|
|
254
287
|
);
|
|
255
288
|
}
|
|
256
289
|
|
|
290
|
+
// Wrapper component that provides all contexts
|
|
291
|
+
function App() {
|
|
292
|
+
return (
|
|
293
|
+
<AuthProvider>
|
|
294
|
+
<ProjectProvider>
|
|
295
|
+
<TelemetryProvider>
|
|
296
|
+
<AppContent />
|
|
297
|
+
</TelemetryProvider>
|
|
298
|
+
</ProjectProvider>
|
|
299
|
+
</AuthProvider>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
257
303
|
// Mount the app
|
|
258
304
|
const root = createRoot(document.getElementById("root")!);
|
|
259
|
-
root.render(
|
|
260
|
-
<TelemetryProvider>
|
|
261
|
-
<App />
|
|
262
|
-
</TelemetryProvider>
|
|
263
|
-
);
|
|
305
|
+
root.render(<App />);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon } from "../common/Icons";
|
|
3
|
-
import { useAgentActivity } from "../../context";
|
|
2
|
+
import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
|
|
3
|
+
import { useAgentActivity, useProjects } from "../../context";
|
|
4
4
|
import type { Agent, AgentFeatures } from "../../types";
|
|
5
5
|
|
|
6
6
|
interface AgentCardProps {
|
|
@@ -8,22 +8,26 @@ interface AgentCardProps {
|
|
|
8
8
|
selected: boolean;
|
|
9
9
|
onSelect: () => void;
|
|
10
10
|
onToggle: (e?: React.MouseEvent) => void;
|
|
11
|
-
|
|
11
|
+
showProject?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const FEATURE_ICONS: { key: keyof AgentFeatures; icon: React.ComponentType<{ className?: string }>; label: string }[] = [
|
|
15
15
|
{ key: "memory", icon: MemoryIcon, label: "Memory" },
|
|
16
16
|
{ key: "tasks", icon: TasksIcon, label: "Tasks" },
|
|
17
|
+
{ key: "files", icon: FilesIcon, label: "Files" },
|
|
17
18
|
{ key: "vision", icon: VisionIcon, label: "Vision" },
|
|
18
19
|
{ key: "operator", icon: OperatorIcon, label: "Operator" },
|
|
19
20
|
{ key: "mcp", icon: McpIcon, label: "MCP" },
|
|
20
21
|
{ key: "realtime", icon: RealtimeIcon, label: "Realtime" },
|
|
22
|
+
{ key: "agents", icon: MultiAgentIcon, label: "Multi-Agent" },
|
|
21
23
|
];
|
|
22
24
|
|
|
23
|
-
export function AgentCard({ agent, selected, onSelect, onToggle,
|
|
25
|
+
export function AgentCard({ agent, selected, onSelect, onToggle, showProject }: AgentCardProps) {
|
|
24
26
|
const enabledFeatures = FEATURE_ICONS.filter(f => agent.features?.[f.key]);
|
|
25
27
|
const mcpServers = agent.mcpServerDetails || [];
|
|
26
28
|
const { isActive, type } = useAgentActivity(agent.id);
|
|
29
|
+
const { projects } = useProjects();
|
|
30
|
+
const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
|
|
27
31
|
|
|
28
32
|
return (
|
|
29
33
|
<div
|
|
@@ -41,6 +45,12 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
|
|
|
41
45
|
{agent.provider} / {agent.model}
|
|
42
46
|
{agent.port && <span className="text-[#444]"> · :{agent.port}</span>}
|
|
43
47
|
</p>
|
|
48
|
+
{showProject && project && (
|
|
49
|
+
<p className="text-sm text-[#666] flex items-center gap-1.5 mt-1">
|
|
50
|
+
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
|
|
51
|
+
{project.name}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
44
54
|
</div>
|
|
45
55
|
<StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityType={type} />
|
|
46
56
|
</div>
|
|
@@ -84,24 +94,16 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
|
|
|
84
94
|
{agent.systemPrompt}
|
|
85
95
|
</p>
|
|
86
96
|
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</button>
|
|
98
|
-
<button
|
|
99
|
-
onClick={onDelete}
|
|
100
|
-
className="px-3 py-1.5 rounded text-sm font-medium bg-red-500/20 text-red-400 hover:bg-red-500/30 transition"
|
|
101
|
-
>
|
|
102
|
-
Delete
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
97
|
+
<button
|
|
98
|
+
onClick={onToggle}
|
|
99
|
+
className={`w-full px-3 py-1.5 rounded text-sm font-medium transition ${
|
|
100
|
+
agent.status === "running"
|
|
101
|
+
? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
|
|
102
|
+
: "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
{agent.status === "running" ? "Stop" : "Start"}
|
|
106
|
+
</button>
|
|
105
107
|
</div>
|
|
106
108
|
);
|
|
107
109
|
}
|