apteva 0.4.17 → 0.4.19

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.
Files changed (78) hide show
  1. package/dist/ActivityPage.9a1qg4bp.js +3 -0
  2. package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
  3. package/dist/App.1nmg2h01.js +4 -0
  4. package/dist/App.5qw2dtxs.js +4 -0
  5. package/dist/App.6nc5acvk.js +4 -0
  6. package/dist/App.7vzbaz56.js +4 -0
  7. package/dist/App.8rfz30p1.js +4 -0
  8. package/dist/App.amwp54wf.js +4 -0
  9. package/dist/App.e4202qb4.js +267 -0
  10. package/dist/App.errxz2q4.js +4 -0
  11. package/dist/App.f8qsyhpr.js +4 -0
  12. package/dist/App.g8vq68n0.js +20 -0
  13. package/dist/App.kfyrnznw.js +13 -0
  14. package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
  15. package/dist/App.p93mmyqw.js +4 -0
  16. package/dist/App.qmg33p02.js +4 -0
  17. package/dist/App.sdsc0258.js +4 -0
  18. package/dist/ConnectionsPage.7zqba1r0.js +3 -0
  19. package/dist/McpPage.kf2g327t.js +3 -0
  20. package/dist/SettingsPage.472c15ep.js +3 -0
  21. package/dist/SkillsPage.xdxnh68a.js +3 -0
  22. package/dist/TasksPage.7g0b8xwc.js +3 -0
  23. package/dist/TelemetryPage.pr7rbz4r.js +3 -0
  24. package/dist/TestsPage.zhc6rqjm.js +3 -0
  25. package/dist/apteva-kit.css +1 -1
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +9 -4
  29. package/src/auth/middleware.ts +2 -0
  30. package/src/channels/index.ts +40 -0
  31. package/src/channels/telegram.ts +306 -0
  32. package/src/db.ts +342 -11
  33. package/src/integrations/agentdojo.ts +1 -1
  34. package/src/mcp-handler.ts +31 -24
  35. package/src/mcp-platform.ts +41 -1
  36. package/src/providers.ts +22 -9
  37. package/src/routes/api/agent-utils.ts +38 -2
  38. package/src/routes/api/agents.ts +65 -2
  39. package/src/routes/api/channels.ts +182 -0
  40. package/src/routes/api/integrations.ts +13 -5
  41. package/src/routes/api/mcp.ts +27 -9
  42. package/src/routes/api/projects.ts +19 -2
  43. package/src/routes/api/system.ts +26 -12
  44. package/src/routes/api/telemetry.ts +30 -0
  45. package/src/routes/api/triggers.ts +478 -0
  46. package/src/routes/api/webhooks.ts +171 -0
  47. package/src/routes/api.ts +7 -1
  48. package/src/routes/static.ts +12 -3
  49. package/src/server.ts +43 -6
  50. package/src/triggers/agentdojo.ts +253 -0
  51. package/src/triggers/composio.ts +264 -0
  52. package/src/triggers/index.ts +71 -0
  53. package/src/tui/AgentList.tsx +145 -0
  54. package/src/tui/App.tsx +102 -0
  55. package/src/tui/Login.tsx +104 -0
  56. package/src/tui/api.ts +72 -0
  57. package/src/tui/index.tsx +7 -0
  58. package/src/web/App.tsx +18 -11
  59. package/src/web/components/agents/AgentCard.tsx +14 -7
  60. package/src/web/components/agents/AgentPanel.tsx +94 -137
  61. package/src/web/components/common/Icons.tsx +16 -0
  62. package/src/web/components/common/index.ts +1 -0
  63. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  64. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  65. package/src/web/components/connections/OverviewTab.tsx +137 -0
  66. package/src/web/components/connections/TriggersTab.tsx +1169 -0
  67. package/src/web/components/index.ts +1 -0
  68. package/src/web/components/layout/Header.tsx +196 -4
  69. package/src/web/components/layout/Sidebar.tsx +7 -1
  70. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  71. package/src/web/components/settings/SettingsPage.tsx +364 -2
  72. package/src/web/components/tasks/TasksPage.tsx +2 -2
  73. package/src/web/components/tests/TestsPage.tsx +1 -2
  74. package/src/web/context/TelemetryContext.tsx +14 -1
  75. package/src/web/context/index.ts +1 -1
  76. package/src/web/hooks/useAgents.ts +15 -11
  77. package/src/web/types.ts +1 -1
  78. package/dist/App.fq4xbpcz.js +0 -228
@@ -0,0 +1,71 @@
1
+ // Generic Trigger Provider Interface
2
+ // Allows multiple providers (Composio, AgentDojo, local, etc.) to offer trigger/webhook integrations
3
+
4
+ export interface TriggerType {
5
+ slug: string;
6
+ name: string;
7
+ description: string;
8
+ type: "webhook" | "poll";
9
+ toolkit_slug: string;
10
+ toolkit_name: string;
11
+ logo: string | null;
12
+ config_schema: Record<string, unknown>;
13
+ payload_schema: Record<string, unknown>;
14
+ }
15
+
16
+ export interface TriggerInstance {
17
+ id: string;
18
+ trigger_slug: string;
19
+ connected_account_id: string | null;
20
+ status: "active" | "disabled";
21
+ config: Record<string, unknown>;
22
+ created_at: string;
23
+ }
24
+
25
+ export interface TriggerProvider {
26
+ id: string;
27
+ name: string;
28
+
29
+ // Browse available trigger types
30
+ listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]>;
31
+ getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null>;
32
+
33
+ // CRUD trigger instances (all remote)
34
+ createTrigger(
35
+ apiKey: string,
36
+ slug: string,
37
+ connectedAccountId: string,
38
+ config?: Record<string, unknown>,
39
+ ): Promise<{ triggerId: string }>;
40
+ listTriggers(apiKey: string): Promise<TriggerInstance[]>;
41
+ enableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
42
+ disableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
43
+ deleteTrigger(apiKey: string, triggerId: string): Promise<boolean>;
44
+
45
+ // Webhook configuration
46
+ setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }>;
47
+ getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }>;
48
+
49
+ // Webhook verification and parsing (each provider signs differently)
50
+ verifyWebhook(req: Request, body: string, secret: string): boolean;
51
+ parseWebhookPayload(body: Record<string, unknown>): {
52
+ triggerSlug: string;
53
+ triggerInstanceId: string | null;
54
+ payload: Record<string, unknown>;
55
+ };
56
+ }
57
+
58
+ // Provider registry
59
+ const triggerProviders: Map<string, TriggerProvider> = new Map();
60
+
61
+ export function registerTriggerProvider(provider: TriggerProvider) {
62
+ triggerProviders.set(provider.id, provider);
63
+ }
64
+
65
+ export function getTriggerProvider(id: string): TriggerProvider | undefined {
66
+ return triggerProviders.get(id);
67
+ }
68
+
69
+ export function getTriggerProviderIds(): string[] {
70
+ return Array.from(triggerProviders.keys());
71
+ }
@@ -0,0 +1,145 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import type { AptevaAPI, Agent, User } from "./api.js";
5
+
6
+ interface AgentListProps {
7
+ api: AptevaAPI;
8
+ user: User;
9
+ }
10
+
11
+ export function AgentList({ api, user }: AgentListProps) {
12
+ const { exit } = useApp();
13
+ const [agents, setAgents] = useState<Agent[]>([]);
14
+ const [loading, setLoading] = useState(true);
15
+ const [selected, setSelected] = useState(0);
16
+
17
+ useEffect(() => {
18
+ const load = async () => {
19
+ const result = await api.getAgents();
20
+ setAgents(result);
21
+ setLoading(false);
22
+ };
23
+ load();
24
+ }, []);
25
+
26
+ useInput((input, key) => {
27
+ if (input === "q") exit();
28
+ if (input === "r") {
29
+ setLoading(true);
30
+ api.getAgents().then(result => {
31
+ setAgents(result);
32
+ setLoading(false);
33
+ });
34
+ }
35
+ if (key.upArrow) setSelected(s => Math.max(0, s - 1));
36
+ if (key.downArrow) setSelected(s => Math.min(agents.length - 1, s + 1));
37
+ });
38
+
39
+ const statusColor = (status: string) => {
40
+ if (status === "running") return "green";
41
+ if (status === "error") return "red";
42
+ return "gray";
43
+ };
44
+
45
+ const statusIcon = (status: string) => {
46
+ if (status === "running") return "●";
47
+ if (status === "error") return "✕";
48
+ return "○";
49
+ };
50
+
51
+ // Column widths
52
+ const nameW = 24;
53
+ const statusW = 12;
54
+ const modelW = 28;
55
+ const providerW = 14;
56
+
57
+ return (
58
+ <Box flexDirection="column" padding={1}>
59
+ {/* Header */}
60
+ <Box marginBottom={1} justifyContent="space-between">
61
+ <Box>
62
+ <Text color="hex('#f97316')" bold>
63
+ {">"}_
64
+ </Text>
65
+ <Text bold> apteva</Text>
66
+ <Text dimColor> — agents</Text>
67
+ </Box>
68
+ <Text dimColor>
69
+ {user.username} ({user.role})
70
+ </Text>
71
+ </Box>
72
+
73
+ {loading ? (
74
+ <Box>
75
+ <Text color="hex('#f97316')">
76
+ <Spinner type="dots" />
77
+ </Text>
78
+ <Text> Loading agents...</Text>
79
+ </Box>
80
+ ) : agents.length === 0 ? (
81
+ <Text dimColor>No agents found.</Text>
82
+ ) : (
83
+ <Box flexDirection="column">
84
+ {/* Table header */}
85
+ <Box>
86
+ <Text bold color="hex('#888')">
87
+ {" "}
88
+ {pad("NAME", nameW)}
89
+ {pad("STATUS", statusW)}
90
+ {pad("MODEL", modelW)}
91
+ {pad("PROVIDER", providerW)}
92
+ </Text>
93
+ </Box>
94
+ <Box marginBottom={0}>
95
+ <Text dimColor>
96
+ {" "}
97
+ {"─".repeat(nameW + statusW + modelW + providerW)}
98
+ </Text>
99
+ </Box>
100
+
101
+ {/* Rows */}
102
+ {agents.map((agent, i) => (
103
+ <Box key={agent.id}>
104
+ <Text color={i === selected ? "hex('#f97316')" : undefined}>
105
+ {i === selected ? "▸ " : " "}
106
+ </Text>
107
+ <Text color={i === selected ? "white" : undefined}>
108
+ {pad(agent.name, nameW)}
109
+ </Text>
110
+ <Text color={statusColor(agent.status)}>
111
+ {statusIcon(agent.status)}{" "}
112
+ {pad(agent.status, statusW - 2)}
113
+ </Text>
114
+ <Text dimColor>
115
+ {pad(agent.model, modelW)}
116
+ </Text>
117
+ <Text dimColor>
118
+ {pad(agent.provider, providerW)}
119
+ </Text>
120
+ </Box>
121
+ ))}
122
+ </Box>
123
+ )}
124
+
125
+ {/* Footer */}
126
+ <Box marginTop={1}>
127
+ <Text dimColor>
128
+ ↑↓ navigate · r refresh · q quit
129
+ </Text>
130
+ </Box>
131
+ <Box>
132
+ <Text dimColor>
133
+ {agents.length} agent{agents.length !== 1 ? "s" : ""}
134
+ {" · "}
135
+ {agents.filter(a => a.status === "running").length} running
136
+ </Text>
137
+ </Box>
138
+ </Box>
139
+ );
140
+ }
141
+
142
+ function pad(str: string, width: number): string {
143
+ if (str.length >= width) return str.slice(0, width - 1) + "…";
144
+ return str + " ".repeat(width - str.length);
145
+ }
@@ -0,0 +1,102 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import { AptevaAPI, type User } from "./api.js";
5
+ import { Login } from "./Login.js";
6
+ import { AgentList } from "./AgentList.js";
7
+ import { spawn } from "child_process";
8
+ import { resolve, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ interface AppProps {
12
+ baseUrl: string;
13
+ }
14
+
15
+ export function App({ baseUrl }: AppProps) {
16
+ const [api] = useState(() => new AptevaAPI(baseUrl));
17
+ const [screen, setScreen] = useState<"connecting" | "login" | "agents">("connecting");
18
+ const [user, setUser] = useState<User | null>(null);
19
+ const [connectError, setConnectError] = useState("");
20
+
21
+ useEffect(() => {
22
+ let cancelled = false;
23
+
24
+ const tryConnect = async () => {
25
+ // First check if server is already running
26
+ const connected = await api.checkConnection();
27
+ if (cancelled) return;
28
+
29
+ if (connected) {
30
+ setScreen("login");
31
+ return;
32
+ }
33
+
34
+ // Server not running — try to start it
35
+ setConnectError("Server not running. Starting...");
36
+
37
+ try {
38
+ // Find the server entry point relative to this file
39
+ const serverPath = resolve(dirname(fileURLToPath(import.meta.url)), "../server.ts");
40
+ const child = spawn("bun", ["run", serverPath], {
41
+ stdio: "ignore",
42
+ detached: true,
43
+ env: { ...process.env, PORT: new URL(baseUrl).port || "4280" },
44
+ });
45
+ child.unref();
46
+
47
+ // Wait for server to come up (poll for up to 10 seconds)
48
+ for (let i = 0; i < 20; i++) {
49
+ if (cancelled) return;
50
+ await new Promise(r => setTimeout(r, 500));
51
+ const up = await api.checkConnection();
52
+ if (up) {
53
+ if (!cancelled) setScreen("login");
54
+ return;
55
+ }
56
+ }
57
+ } catch {
58
+ // Spawn failed
59
+ }
60
+
61
+ if (!cancelled) {
62
+ setConnectError(`Cannot connect to ${baseUrl}. Start the server with: bun run dev`);
63
+ }
64
+ };
65
+
66
+ tryConnect();
67
+ return () => { cancelled = true; };
68
+ }, []);
69
+
70
+ if (screen === "connecting") {
71
+ return (
72
+ <Box flexDirection="column" padding={1}>
73
+ <Box marginBottom={1}>
74
+ <Text color="hex('#f97316')" bold>{">"}_</Text>
75
+ <Text bold> apteva</Text>
76
+ </Box>
77
+ <Box>
78
+ <Text color="hex('#f97316')"><Spinner type="dots" /></Text>
79
+ <Text> {connectError || `Connecting to ${baseUrl}...`}</Text>
80
+ </Box>
81
+ </Box>
82
+ );
83
+ }
84
+
85
+ if (screen === "login") {
86
+ return (
87
+ <Login
88
+ api={api}
89
+ onSuccess={(u) => {
90
+ setUser(u);
91
+ setScreen("agents");
92
+ }}
93
+ />
94
+ );
95
+ }
96
+
97
+ if (screen === "agents" && user) {
98
+ return <AgentList api={api} user={user} />;
99
+ }
100
+
101
+ return null;
102
+ }
@@ -0,0 +1,104 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import Spinner from "ink-spinner";
5
+ import type { AptevaAPI, User } from "./api.js";
6
+
7
+ interface LoginProps {
8
+ api: AptevaAPI;
9
+ onSuccess: (user: User) => void;
10
+ }
11
+
12
+ export function Login({ api, onSuccess }: LoginProps) {
13
+ const [field, setField] = useState<"username" | "password">("username");
14
+ const [username, setUsername] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [error, setError] = useState("");
17
+ const [loading, setLoading] = useState(false);
18
+
19
+ useInput((input, key) => {
20
+ if (key.tab || (key.return && field === "username" && username)) {
21
+ setField(field === "username" ? "password" : "username");
22
+ }
23
+ });
24
+
25
+ const handleSubmit = async () => {
26
+ if (!username || !password) return;
27
+ setLoading(true);
28
+ setError("");
29
+ const result = await api.login(username, password);
30
+ setLoading(false);
31
+ if (result.success && result.user) {
32
+ onSuccess(result.user);
33
+ } else {
34
+ setError(result.error || "Login failed");
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Box flexDirection="column" padding={1}>
40
+ <Box marginBottom={1}>
41
+ <Text color="hex('#f97316')" bold>
42
+ {">"}_
43
+ </Text>
44
+ <Text bold> apteva</Text>
45
+ </Box>
46
+
47
+ <Box marginBottom={1}>
48
+ <Text dimColor>Sign in to continue</Text>
49
+ </Box>
50
+
51
+ <Box>
52
+ <Text color={field === "username" ? "hex('#f97316')" : "white"}>
53
+ Username:{" "}
54
+ </Text>
55
+ {field === "username" ? (
56
+ <TextInput
57
+ value={username}
58
+ onChange={setUsername}
59
+ onSubmit={() => {
60
+ if (username) setField("password");
61
+ }}
62
+ />
63
+ ) : (
64
+ <Text>{username}</Text>
65
+ )}
66
+ </Box>
67
+
68
+ <Box>
69
+ <Text color={field === "password" ? "hex('#f97316')" : "white"}>
70
+ Password:{" "}
71
+ </Text>
72
+ {field === "password" ? (
73
+ <TextInput
74
+ value={password}
75
+ onChange={setPassword}
76
+ onSubmit={handleSubmit}
77
+ mask="*"
78
+ />
79
+ ) : (
80
+ <Text dimColor>{"*".repeat(password.length) || "..."}</Text>
81
+ )}
82
+ </Box>
83
+
84
+ {loading && (
85
+ <Box marginTop={1}>
86
+ <Text color="hex('#f97316')">
87
+ <Spinner type="dots" />
88
+ </Text>
89
+ <Text> Signing in...</Text>
90
+ </Box>
91
+ )}
92
+
93
+ {error && (
94
+ <Box marginTop={1}>
95
+ <Text color="red">{error}</Text>
96
+ </Box>
97
+ )}
98
+
99
+ <Box marginTop={1}>
100
+ <Text dimColor>Tab to switch fields · Enter to submit</Text>
101
+ </Box>
102
+ </Box>
103
+ );
104
+ }
package/src/tui/api.ts ADDED
@@ -0,0 +1,72 @@
1
+ export interface Agent {
2
+ id: string;
3
+ name: string;
4
+ model: string;
5
+ provider: string;
6
+ status: "running" | "stopped" | "error";
7
+ port: number | null;
8
+ projectId: string | null;
9
+ }
10
+
11
+ export interface User {
12
+ id: string;
13
+ username: string;
14
+ role: string;
15
+ }
16
+
17
+ export class AptevaAPI {
18
+ baseUrl: string;
19
+ private token: string | null = null;
20
+
21
+ constructor(baseUrl: string) {
22
+ this.baseUrl = baseUrl.replace(/\/$/, "");
23
+ }
24
+
25
+ async checkConnection(): Promise<boolean> {
26
+ try {
27
+ const res = await fetch(`${this.baseUrl}/api/auth/check`, { signal: AbortSignal.timeout(3000) });
28
+ return res.ok;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ private async fetch(path: string, opts: RequestInit = {}): Promise<Response> {
35
+ const headers: Record<string, string> = {
36
+ "Content-Type": "application/json",
37
+ ...(opts.headers as Record<string, string> || {}),
38
+ };
39
+ if (this.token) {
40
+ headers.Authorization = `Bearer ${this.token}`;
41
+ }
42
+ return fetch(`${this.baseUrl}${path}`, { ...opts, headers });
43
+ }
44
+
45
+ async login(username: string, password: string): Promise<{ success: boolean; user?: User; error?: string }> {
46
+ try {
47
+ const res = await this.fetch("/api/auth/login", {
48
+ method: "POST",
49
+ body: JSON.stringify({ username, password }),
50
+ });
51
+ const data = await res.json();
52
+ if (!res.ok) {
53
+ return { success: false, error: data.error || "Login failed" };
54
+ }
55
+ this.token = data.accessToken;
56
+ return { success: true, user: data.user };
57
+ } catch (err: any) {
58
+ return { success: false, error: err.message || "Connection failed" };
59
+ }
60
+ }
61
+
62
+ async getAgents(): Promise<Agent[]> {
63
+ try {
64
+ const res = await this.fetch("/api/agents");
65
+ if (!res.ok) return [];
66
+ const data = await res.json();
67
+ return data.agents || [];
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { App } from "./App.js";
4
+
5
+ const baseUrl = process.argv[2] || "http://localhost:4280";
6
+
7
+ render(<App baseUrl={baseUrl} />);
package/src/web/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import "@apteva/apteva-kit/styles.css";
4
4
 
@@ -12,28 +12,31 @@ import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects,
12
12
  // Hooks
13
13
  import { useAgents, useProviders, useOnboarding } from "./hooks";
14
14
 
15
- // Components
15
+ // Core components (always needed)
16
16
  import {
17
17
  LoadingSpinner,
18
18
  Header,
19
19
  Sidebar,
20
20
  ErrorBanner,
21
21
  OnboardingWizard,
22
- SettingsPage,
23
22
  CreateAgentModal,
24
23
  AgentsView,
25
24
  Dashboard,
26
- ActivityPage,
27
- TasksPage,
28
- McpPage,
29
- SkillsPage,
30
- TestsPage,
31
- TelemetryPage,
32
25
  LoginPage,
33
26
  } from "./components";
34
- import { ApiDocsPage } from "./components/api/ApiDocsPage";
35
27
  import { MetaAgentProvider, MetaAgentPanel } from "./components/meta-agent/MetaAgent";
36
28
 
29
+ // Lazy-loaded page components (only loaded when navigated to)
30
+ const SettingsPage = lazy(() => import("./components/settings/SettingsPage").then(m => ({ default: m.SettingsPage })));
31
+ const ActivityPage = lazy(() => import("./components/activity/ActivityPage").then(m => ({ default: m.ActivityPage })));
32
+ const TasksPage = lazy(() => import("./components/tasks/TasksPage").then(m => ({ default: m.TasksPage })));
33
+ const McpPage = lazy(() => import("./components/mcp/McpPage").then(m => ({ default: m.McpPage })));
34
+ const SkillsPage = lazy(() => import("./components/skills/SkillsPage").then(m => ({ default: m.SkillsPage })));
35
+ const TestsPage = lazy(() => import("./components/tests/TestsPage").then(m => ({ default: m.TestsPage })));
36
+ const TelemetryPage = lazy(() => import("./components/telemetry/TelemetryPage").then(m => ({ default: m.TelemetryPage })));
37
+ const ConnectionsPage = lazy(() => import("./components/connections/ConnectionsPage").then(m => ({ default: m.ConnectionsPage })));
38
+ const ApiDocsPage = lazy(() => import("./components/api/ApiDocsPage").then(m => ({ default: m.ApiDocsPage })));
39
+
37
40
  function AppContent() {
38
41
  // Auth state
39
42
  const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
@@ -237,7 +240,7 @@ function AppContent() {
237
240
 
238
241
  return (
239
242
  <div className="h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col overflow-hidden">
240
- <Header onMenuClick={() => setMobileMenuOpen(true)} />
243
+ <Header onMenuClick={() => setMobileMenuOpen(true)} agents={agents} />
241
244
 
242
245
  {startError && (
243
246
  <ErrorBanner message={startError} onDismiss={() => setStartError(null)} />
@@ -254,6 +257,7 @@ function AppContent() {
254
257
  />
255
258
 
256
259
  <main className="flex-1 overflow-hidden flex">
260
+ <Suspense fallback={<LoadingSpinner />}>
257
261
  {route === "settings" && <SettingsPage />}
258
262
 
259
263
  {route === "activity" && (
@@ -292,6 +296,8 @@ function AppContent() {
292
296
 
293
297
  {route === "tasks" && <TasksPage />}
294
298
 
299
+ {route === "connections" && <ConnectionsPage />}
300
+
295
301
  {route === "mcp" && <McpPage />}
296
302
 
297
303
  {route === "skills" && <SkillsPage />}
@@ -301,6 +307,7 @@ function AppContent() {
301
307
  {route === "telemetry" && <TelemetryPage />}
302
308
 
303
309
  {route === "api" && <ApiDocsPage />}
310
+ </Suspense>
304
311
  </main>
305
312
  </div>
306
313
 
@@ -121,13 +121,16 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
121
121
 
122
122
  <button
123
123
  onClick={onToggle}
124
+ disabled={agent.status === "starting" || agent.status === "stopping"}
124
125
  className={`w-full px-3 py-1.5 rounded text-sm font-medium transition mt-auto ${
125
- agent.status === "running"
126
- ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
127
- : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
126
+ agent.status === "starting" || agent.status === "stopping"
127
+ ? "bg-[#333] text-[#666] cursor-wait"
128
+ : agent.status === "running"
129
+ ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
130
+ : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
128
131
  }`}
129
132
  >
130
- {agent.status === "running" ? "Stop" : "Start"}
133
+ {agent.status === "starting" ? "Starting..." : agent.status === "stopping" ? "Stopping..." : agent.status === "running" ? "Stop" : "Start"}
131
134
  </button>
132
135
  </div>
133
136
  );
@@ -142,12 +145,16 @@ function StatusBadge({ status, isActive, activityType }: { status: Agent["status
142
145
  );
143
146
  }
144
147
 
148
+ const isTransitioning = status === "starting" || status === "stopping";
149
+
145
150
  return (
146
151
  <span
147
152
  className={`px-2 py-1 rounded text-xs font-medium ${
148
- status === "running"
149
- ? "bg-[#3b82f6]/20 text-[#3b82f6]"
150
- : "bg-[#333] text-[#666]"
153
+ isTransitioning
154
+ ? "bg-yellow-500/20 text-yellow-400 animate-pulse"
155
+ : status === "running"
156
+ ? "bg-[#3b82f6]/20 text-[#3b82f6]"
157
+ : "bg-[#333] text-[#666]"
151
158
  }`}
152
159
  >
153
160
  {status}