apteva 0.4.20 → 0.4.29

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 (67) hide show
  1. package/dist/ActivityPage.41nbye4r.js +3 -0
  2. package/dist/{ApiDocsPage.kf6bbwkk.js → ApiDocsPage.4smnt8m3.js} +2 -2
  3. package/dist/{App.jfx3der4.js → App.0sbax9et.js} +3 -3
  4. package/dist/App.0ws427h8.js +4 -0
  5. package/dist/App.4ehxpt48.js +4 -0
  6. package/dist/App.6q6bar8b.js +4 -0
  7. package/dist/App.ca1rz1ph.js +4 -0
  8. package/dist/{App.7v1w3ys9.js → App.ensa6z0r.js} +3 -3
  9. package/dist/{App.n4jb3c22.js → App.f8g7tych.js} +3 -3
  10. package/dist/App.kh7d2xj3.js +267 -0
  11. package/dist/App.mvtqv6qc.js +20 -0
  12. package/dist/{App.c90t3dxg.js → App.ncgc9cxy.js} +3 -3
  13. package/dist/{App.039re6cf.js → App.p0fb1pds.js} +3 -3
  14. package/dist/App.pmaq48sj.js +4 -0
  15. package/dist/{App.2yy66bnp.js → App.yv87t9m5.js} +3 -3
  16. package/dist/App.zjmfm8p6.js +4 -0
  17. package/dist/ConnectionsPage.anb3rv9a.js +3 -0
  18. package/dist/McpPage.y396h6fy.js +3 -0
  19. package/dist/SettingsPage.5k6vp396.js +3 -0
  20. package/dist/SkillsPage.yj3xdsay.js +3 -0
  21. package/dist/TasksPage.sjv0khtv.js +3 -0
  22. package/dist/TelemetryPage.2qm4w16r.js +3 -0
  23. package/dist/TestsPage.zzs4qfj8.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +2 -2
  27. package/src/channels/telegram.ts +5 -0
  28. package/src/crypto.ts +13 -4
  29. package/src/db.ts +25 -2
  30. package/src/integrations/agentdojo.ts +1 -1
  31. package/src/providers.ts +46 -0
  32. package/src/routes/api/agent-utils.ts +64 -9
  33. package/src/routes/api/agents.ts +41 -13
  34. package/src/routes/api/integrations.ts +16 -6
  35. package/src/routes/api/mcp.ts +7 -0
  36. package/src/routes/api/triggers.ts +45 -5
  37. package/src/web/App.tsx +1 -0
  38. package/src/web/components/activity/ActivityPage.tsx +349 -214
  39. package/src/web/components/agents/AgentCard.tsx +37 -8
  40. package/src/web/components/agents/AgentPanel.tsx +268 -23
  41. package/src/web/components/connections/IntegrationsTab.tsx +57 -31
  42. package/src/web/components/connections/TriggersTab.tsx +336 -159
  43. package/src/web/components/dashboard/Dashboard.tsx +39 -7
  44. package/src/web/components/layout/Header.tsx +0 -34
  45. package/src/web/components/layout/Sidebar.tsx +43 -3
  46. package/src/web/components/mcp/McpPage.tsx +16 -5
  47. package/src/web/components/settings/SettingsPage.tsx +279 -30
  48. package/src/web/components/tasks/TasksPage.tsx +32 -6
  49. package/src/web/context/ProjectContext.tsx +5 -0
  50. package/src/web/context/TelemetryContext.tsx +14 -0
  51. package/src/web/types.ts +20 -2
  52. package/dist/ActivityPage.h769ek3a.js +0 -3
  53. package/dist/App.2jmkqm8c.js +0 -4
  54. package/dist/App.3515wsb4.js +0 -4
  55. package/dist/App.edwahsvz.js +0 -4
  56. package/dist/App.q3bpx15d.js +0 -20
  57. package/dist/App.r0a2nmqs.js +0 -267
  58. package/dist/App.s2yrcz15.js +0 -4
  59. package/dist/App.s5j82a5j.js +0 -4
  60. package/dist/App.tg1b94tx.js +0 -4
  61. package/dist/ConnectionsPage.a67fjgbf.js +0 -3
  62. package/dist/McpPage.d4p3xvtk.js +0 -3
  63. package/dist/SettingsPage.46sqpe39.js +0 -3
  64. package/dist/SkillsPage.j9hkqm99.js +0 -3
  65. package/dist/TasksPage.6pvkb7s7.js +0 -3
  66. package/dist/TelemetryPage.5zq9msb5.js +0 -3
  67. package/dist/TestsPage.24432yqt.js +0 -3
@@ -1,6 +1,6 @@
1
- import React from "react";
2
- import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, SkillsIcon } from "../common/Icons";
3
- import { useAgentActivity, useProjects } from "../../context";
1
+ import React, { useState, useEffect } from "react";
2
+ import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, SkillsIcon, ActivityIcon } from "../common/Icons";
3
+ import { useAgentActivity, useProjects, useAuth } from "../../context";
4
4
  import type { Agent, AgentFeatures } from "../../types";
5
5
 
6
6
  interface AgentCardProps {
@@ -26,9 +26,18 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
26
26
  const enabledFeatures = FEATURE_ICONS.filter(f => agent.features?.[f.key]);
27
27
  const mcpServers = agent.mcpServerDetails || [];
28
28
  const skills = agent.skillDetails || [];
29
- const { isActive, type } = useAgentActivity(agent.id);
29
+ const { isActive, label: activityLabel } = useAgentActivity(agent.id);
30
30
  const { projects } = useProjects();
31
+ const { authFetch } = useAuth();
31
32
  const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
33
+ const [subscriptions, setSubscriptions] = useState<{ id: string; trigger_slug: string; enabled: boolean }[]>([]);
34
+
35
+ useEffect(() => {
36
+ authFetch(`/api/subscriptions?agent_id=${agent.id}`)
37
+ .then(res => res.ok ? res.json() : { subscriptions: [] })
38
+ .then(data => setSubscriptions(data.subscriptions || []))
39
+ .catch(() => {});
40
+ }, [agent.id, authFetch]);
32
41
 
33
42
  return (
34
43
  <div
@@ -53,7 +62,7 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
53
62
  </p>
54
63
  )}
55
64
  </div>
56
- <StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityType={type} />
65
+ <StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityLabel={activityLabel} />
57
66
  </div>
58
67
 
59
68
  {enabledFeatures.length > 0 && (
@@ -115,6 +124,26 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
115
124
  </div>
116
125
  )}
117
126
 
127
+ {/* Subscriptions (triggers listening to) */}
128
+ {subscriptions.length > 0 && (
129
+ <div className="flex flex-wrap gap-1.5 mb-3">
130
+ {subscriptions.map((sub) => (
131
+ <span
132
+ key={sub.id}
133
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
134
+ sub.enabled
135
+ ? "bg-cyan-500/10 text-cyan-400"
136
+ : "bg-[#222] text-[#666]"
137
+ }`}
138
+ title={`Trigger: ${sub.trigger_slug.replace(/_/g, " ")}`}
139
+ >
140
+ <ActivityIcon className="w-3 h-3" />
141
+ {sub.trigger_slug.replace(/_/g, " ")}
142
+ </span>
143
+ ))}
144
+ </div>
145
+ )}
146
+
118
147
  <p className="text-sm text-[#666] line-clamp-2 mb-4 flex-1">
119
148
  {agent.systemPrompt}
120
149
  </p>
@@ -136,11 +165,11 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
136
165
  );
137
166
  }
138
167
 
139
- function StatusBadge({ status, isActive, activityType }: { status: Agent["status"]; isActive?: boolean; activityType?: string }) {
140
- if (status === "running" && isActive && activityType) {
168
+ function StatusBadge({ status, isActive, activityLabel }: { status: Agent["status"]; isActive?: boolean; activityLabel?: string }) {
169
+ if (status === "running" && isActive && activityLabel) {
141
170
  return (
142
171
  <span className="px-2 py-1 rounded text-xs font-medium bg-green-500/20 text-green-400 animate-pulse">
143
- {activityType}
172
+ {activityLabel}
144
173
  </span>
145
174
  );
146
175
  }
@@ -1,13 +1,13 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Chat, convertApiMessages } from "@apteva/apteva-kit";
3
3
  import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
4
- import { formatCron, formatRelativeTime } from "../tasks/TasksPage";
4
+ import { formatCron, formatRelativeTime, TrajectoryView } from "../tasks/TasksPage";
5
5
  import { Select } from "../common/Select";
6
6
  import { useConfirm } from "../common/Modal";
7
7
  import { useTelemetry } from "../../context";
8
8
  import { useAuth } from "../../context";
9
- import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, AgentMode, MultiAgentConfig } from "../../types";
10
- import { getMultiAgentConfig } from "../../types";
9
+ import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, AgentMode, MultiAgentConfig, OperatorConfig, Task } from "../../types";
10
+ import { getMultiAgentConfig, getOperatorConfig } from "../../types";
11
11
 
12
12
  type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
13
13
 
@@ -314,10 +314,13 @@ function ThreadsTab({ agent }: { agent: Agent }) {
314
314
  }
315
315
 
316
316
  function TasksTab({ agent }: { agent: Agent }) {
317
- const [tasks, setTasks] = useState<any[]>([]);
317
+ const { authFetch } = useAuth();
318
+ const [tasks, setTasks] = useState<Task[]>([]);
318
319
  const [loading, setLoading] = useState(true);
319
320
  const [error, setError] = useState<string | null>(null);
320
321
  const [filter, setFilter] = useState<string>("all");
322
+ const [selectedTask, setSelectedTask] = useState<Task | null>(null);
323
+ const [loadingTask, setLoadingTask] = useState(false);
321
324
  const { events } = useTelemetry({ agent_id: agent.id, category: "task" });
322
325
 
323
326
  // Reset state when agent changes
@@ -325,6 +328,7 @@ function TasksTab({ agent }: { agent: Agent }) {
325
328
  setTasks([]);
326
329
  setError(null);
327
330
  setLoading(true);
331
+ setSelectedTask(null);
328
332
  }, [agent.id]);
329
333
 
330
334
  const fetchTasks = async () => {
@@ -346,6 +350,24 @@ function TasksTab({ agent }: { agent: Agent }) {
346
350
  }
347
351
  };
348
352
 
353
+ const selectTask = async (task: Task) => {
354
+ setSelectedTask(task);
355
+ setLoadingTask(true);
356
+ try {
357
+ const res = await authFetch(`/api/tasks/${task.agentId || agent.id}/${task.id}`);
358
+ if (res.ok) {
359
+ const data = await res.json();
360
+ if (data.task) {
361
+ setSelectedTask({ ...data.task, agentId: task.agentId || agent.id, agentName: task.agentName || agent.name });
362
+ }
363
+ }
364
+ } catch (e) {
365
+ console.error("Failed to fetch task details:", e);
366
+ } finally {
367
+ setLoadingTask(false);
368
+ }
369
+ };
370
+
349
371
  // Refetch when agent changes, filter changes, or task telemetry arrives
350
372
  useEffect(() => {
351
373
  setLoading(true);
@@ -403,6 +425,133 @@ function TasksTab({ agent }: { agent: Agent }) {
403
425
  { value: "failed", label: "Failed" },
404
426
  ];
405
427
 
428
+ // Show task detail view when a task is selected
429
+ if (selectedTask) {
430
+ return (
431
+ <div className="flex-1 flex flex-col overflow-hidden">
432
+ {/* Back button */}
433
+ <div className="px-4 pt-3 pb-2 border-b border-[#1a1a1a] shrink-0">
434
+ <button
435
+ onClick={() => setSelectedTask(null)}
436
+ className="text-sm text-[#666] hover:text-[#e0e0e0] transition flex items-center gap-1"
437
+ >
438
+ <span>←</span> Back to tasks
439
+ </button>
440
+ </div>
441
+
442
+ {/* Task detail content */}
443
+ <div className="flex-1 overflow-auto p-4 space-y-4">
444
+ {/* Title & Status */}
445
+ <div>
446
+ <div className="flex items-start justify-between gap-2 mb-1">
447
+ <h3 className="text-lg font-medium">{selectedTask.title}</h3>
448
+ <span className={`px-2 py-1 rounded text-xs font-medium flex-shrink-0 ${statusColors[selectedTask.status]}`}>
449
+ {selectedTask.status}
450
+ </span>
451
+ </div>
452
+ </div>
453
+
454
+ {/* Description */}
455
+ {selectedTask.description && (
456
+ <div>
457
+ <h4 className="text-xs text-[#666] uppercase tracking-wider mb-1">Description</h4>
458
+ <p className="text-sm text-[#888] whitespace-pre-wrap">{selectedTask.description}</p>
459
+ </div>
460
+ )}
461
+
462
+ {/* Metadata */}
463
+ <div className="grid grid-cols-2 gap-3 text-sm">
464
+ <div>
465
+ <span className="text-[#666]">Type</span>
466
+ <p className="capitalize">{selectedTask.type}</p>
467
+ </div>
468
+ <div>
469
+ <span className="text-[#666]">Priority</span>
470
+ <p>{selectedTask.priority}</p>
471
+ </div>
472
+ {selectedTask.recurrence && (
473
+ <div>
474
+ <span className="text-[#666]">Recurrence</span>
475
+ <p>{formatCron(selectedTask.recurrence)}</p>
476
+ <p className="text-xs text-[#444] mt-0.5 font-mono">{selectedTask.recurrence}</p>
477
+ </div>
478
+ )}
479
+ </div>
480
+
481
+ {/* Timestamps */}
482
+ <div className="space-y-2 text-sm">
483
+ <div className="flex justify-between">
484
+ <span className="text-[#666]">Created</span>
485
+ <span>{new Date(selectedTask.created_at).toLocaleString()}</span>
486
+ </div>
487
+ {selectedTask.execute_at && (
488
+ <div className="flex justify-between">
489
+ <span className="text-[#666]">Scheduled</span>
490
+ <span className="text-[#f97316]">{formatRelativeTime(selectedTask.execute_at)}</span>
491
+ </div>
492
+ )}
493
+ {selectedTask.executed_at && (
494
+ <div className="flex justify-between">
495
+ <span className="text-[#666]">Started</span>
496
+ <span>{new Date(selectedTask.executed_at).toLocaleString()}</span>
497
+ </div>
498
+ )}
499
+ {selectedTask.completed_at && (
500
+ <div className="flex justify-between">
501
+ <span className="text-[#666]">Completed</span>
502
+ <span>{new Date(selectedTask.completed_at).toLocaleString()}</span>
503
+ </div>
504
+ )}
505
+ {selectedTask.next_run && (
506
+ <div className="flex justify-between">
507
+ <span className="text-[#666]">Next Run</span>
508
+ <span className="text-[#f97316]">{formatRelativeTime(selectedTask.next_run)}</span>
509
+ </div>
510
+ )}
511
+ </div>
512
+
513
+ {/* Error */}
514
+ {selectedTask.status === "failed" && selectedTask.error && (
515
+ <div className="min-w-0">
516
+ <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
517
+ <div className="bg-red-500/10 border border-red-500/20 rounded p-3 overflow-x-auto">
518
+ <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{selectedTask.error}</pre>
519
+ </div>
520
+ </div>
521
+ )}
522
+
523
+ {/* Result */}
524
+ {selectedTask.status === "completed" && selectedTask.result && (
525
+ <div className="min-w-0">
526
+ <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
527
+ <div className="bg-green-500/10 border border-green-500/20 rounded p-3 overflow-x-auto">
528
+ <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
529
+ {typeof selectedTask.result === "string" ? selectedTask.result : JSON.stringify(selectedTask.result, null, 2)}
530
+ </pre>
531
+ </div>
532
+ </div>
533
+ )}
534
+
535
+ {/* Trajectory */}
536
+ {loadingTask && !selectedTask.trajectory && (
537
+ <div>
538
+ <h4 className="text-xs text-[#666] uppercase tracking-wider mb-2">Trajectory</h4>
539
+ <div className="text-sm text-[#555]">Loading trajectory...</div>
540
+ </div>
541
+ )}
542
+ {selectedTask.trajectory && selectedTask.trajectory.length > 0 && (
543
+ <div>
544
+ <h4 className="text-xs text-[#666] uppercase tracking-wider mb-2">
545
+ Trajectory ({selectedTask.trajectory.length} steps)
546
+ </h4>
547
+ <TrajectoryView trajectory={selectedTask.trajectory} />
548
+ </div>
549
+ )}
550
+ </div>
551
+ </div>
552
+ );
553
+ }
554
+
406
555
  return (
407
556
  <div className="flex-1 overflow-auto p-4">
408
557
  {/* Filter tabs */}
@@ -431,7 +580,11 @@ function TasksTab({ agent }: { agent: Agent }) {
431
580
  ) : (
432
581
  <div className="space-y-3">
433
582
  {tasks.map(task => (
434
- <div key={task.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
583
+ <div
584
+ key={task.id}
585
+ onClick={() => selectTask(task)}
586
+ className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 cursor-pointer hover:border-[#333] transition"
587
+ >
435
588
  <div className="flex items-start justify-between mb-2">
436
589
  <div className="flex-1 min-w-0">
437
590
  <h3 className="font-medium">{task.title || task.name}</h3>
@@ -970,7 +1123,17 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
970
1123
  const [availableMcpServers, setAvailableMcpServers] = useState<McpServer[]>([]);
971
1124
  const [availableSkills, setAvailableSkills] = useState<AvailableSkill[]>([]);
972
1125
  const [apiKey, setApiKey] = useState<string | null>(null);
1126
+ const [apiKeyFull, setApiKeyFull] = useState<string | null>(null);
973
1127
  const [showApiKey, setShowApiKey] = useState(false);
1128
+ const [subscriptions, setSubscriptions] = useState<{ id: string; trigger_slug: string; enabled: boolean }[]>([]);
1129
+
1130
+ // Fetch subscriptions for this agent
1131
+ useEffect(() => {
1132
+ authFetch(`/api/subscriptions?agent_id=${agent.id}`)
1133
+ .then(res => res.ok ? res.json() : { subscriptions: [] })
1134
+ .then(data => setSubscriptions(data.subscriptions || []))
1135
+ .catch(() => {});
1136
+ }, [agent.id, authFetch]);
974
1137
 
975
1138
  // Fetch available MCP servers
976
1139
  useEffect(() => {
@@ -986,22 +1149,22 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
986
1149
  fetchMcpServers();
987
1150
  }, [authFetch]);
988
1151
 
989
- // Fetch API key (dev mode only)
1152
+ // Fetch API key
990
1153
  useEffect(() => {
991
- if (!isDev) return;
992
1154
  const fetchApiKey = async () => {
993
1155
  try {
994
1156
  const res = await authFetch(`/api/agents/${agent.id}/api-key`);
995
1157
  if (res.ok) {
996
1158
  const data = await res.json();
997
1159
  setApiKey(data.apiKey);
1160
+ setApiKeyFull(data.fullKey || null);
998
1161
  }
999
1162
  } catch (e) {
1000
1163
  // Ignore - not critical
1001
1164
  }
1002
1165
  };
1003
1166
  fetchApiKey();
1004
- }, [agent.id, isDev, authFetch]);
1167
+ }, [agent.id, authFetch]);
1005
1168
 
1006
1169
  // Fetch available skills
1007
1170
  useEffect(() => {
@@ -1055,19 +1218,13 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1055
1218
  const toggleFeature = (key: keyof AgentFeatures) => {
1056
1219
  if (key === "agents") {
1057
1220
  // Special handling for agents feature - convert to MultiAgentConfig
1058
- const current = prev => {
1059
- const agentConfig = getMultiAgentConfig(prev.features, agent.projectId);
1060
- return agentConfig.enabled;
1061
- };
1062
1221
  setForm(prev => {
1063
1222
  const isEnabled = typeof prev.features.agents === "boolean"
1064
1223
  ? prev.features.agents
1065
1224
  : (prev.features.agents as MultiAgentConfig)?.enabled ?? false;
1066
1225
  if (isEnabled) {
1067
- // Turning off - set to false
1068
1226
  return { ...prev, features: { ...prev.features, agents: false } };
1069
1227
  } else {
1070
- // Turning on - set to config with defaults
1071
1228
  return {
1072
1229
  ...prev,
1073
1230
  features: {
@@ -1077,6 +1234,22 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1077
1234
  };
1078
1235
  }
1079
1236
  });
1237
+ } else if (key === "operator") {
1238
+ // Special handling for operator feature - convert to OperatorConfig
1239
+ setForm(prev => {
1240
+ const opConfig = getOperatorConfig(prev.features);
1241
+ if (opConfig.enabled) {
1242
+ return { ...prev, features: { ...prev.features, operator: false } };
1243
+ } else {
1244
+ return {
1245
+ ...prev,
1246
+ features: {
1247
+ ...prev.features,
1248
+ operator: { enabled: true },
1249
+ },
1250
+ };
1251
+ }
1252
+ });
1080
1253
  } else {
1081
1254
  setForm(prev => ({
1082
1255
  ...prev,
@@ -1112,6 +1285,33 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1112
1285
  return config.mode || "worker";
1113
1286
  };
1114
1287
 
1288
+ // Helper to check if operator feature is enabled
1289
+ const isOperatorEnabled = () => {
1290
+ return getOperatorConfig(form.features).enabled;
1291
+ };
1292
+
1293
+ // Get current operator config
1294
+ const getOperatorCfg = (): OperatorConfig => {
1295
+ return getOperatorConfig(form.features);
1296
+ };
1297
+
1298
+ // Get browser providers from the providers list
1299
+ const browserProviders = providers.filter(p => p.type === "browser" && p.hasKey);
1300
+
1301
+ // Set operator browser provider
1302
+ const setOperatorBrowserProvider = (browserProvider: string) => {
1303
+ setForm(prev => {
1304
+ const current = getOperatorConfig(prev.features);
1305
+ return {
1306
+ ...prev,
1307
+ features: {
1308
+ ...prev.features,
1309
+ operator: { ...current, enabled: true, browser_provider: browserProvider },
1310
+ },
1311
+ };
1312
+ });
1313
+ };
1314
+
1115
1315
  const toggleMcpServer = (serverId: string) => {
1116
1316
  setForm(prev => ({
1117
1317
  ...prev,
@@ -1191,8 +1391,10 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1191
1391
  <FormField label="Features">
1192
1392
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
1193
1393
  {FEATURE_CONFIG.map(({ key, label, description, icon: Icon }) => {
1194
- // For agents feature, check the enabled property of the config
1195
- const isEnabled = key === "agents" ? isAgentsEnabled() : !!form.features[key];
1394
+ // For agents/operator features, check the enabled property of the config
1395
+ const isEnabled = key === "agents" ? isAgentsEnabled()
1396
+ : key === "operator" ? isOperatorEnabled()
1397
+ : !!form.features[key];
1196
1398
  return (
1197
1399
  <button
1198
1400
  key={key}
@@ -1258,6 +1460,29 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1258
1460
  </FormField>
1259
1461
  )}
1260
1462
 
1463
+ {/* Operator Browser Provider - shown when operator is enabled */}
1464
+ {isOperatorEnabled() && (
1465
+ <FormField label="Browser Provider">
1466
+ {browserProviders.length > 0 ? (
1467
+ <Select
1468
+ value={getOperatorCfg().browser_provider || ""}
1469
+ options={[
1470
+ { value: "", label: "Auto (first available)" },
1471
+ ...browserProviders.map(p => ({
1472
+ value: p.id,
1473
+ label: p.name,
1474
+ })),
1475
+ ]}
1476
+ onChange={(value) => setOperatorBrowserProvider(value)}
1477
+ />
1478
+ ) : (
1479
+ <p className="text-sm text-[#666] p-3 border border-[#222] rounded bg-[#0a0a0a]">
1480
+ No browser providers configured. Go to Settings &rarr; Providers to add one.
1481
+ </p>
1482
+ )}
1483
+ </FormField>
1484
+ )}
1485
+
1261
1486
  {/* Agent Built-in Tools - Anthropic only */}
1262
1487
  {form.provider === "anthropic" && (
1263
1488
  <FormField label="Agent Built-in Tools">
@@ -1443,8 +1668,30 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1443
1668
  </p>
1444
1669
  )}
1445
1670
 
1671
+ {/* Subscriptions */}
1672
+ <div className="mt-8 pt-6 border-t border-[#222]">
1673
+ <p className="text-sm text-[#666] mb-3">Subscriptions</p>
1674
+ {subscriptions.length === 0 ? (
1675
+ <p className="text-xs text-[#555]">No subscriptions. Set up triggers in Connections to have this agent listen to external events.</p>
1676
+ ) : (
1677
+ <div className="space-y-2">
1678
+ {subscriptions.map(sub => (
1679
+ <div key={sub.id} className="flex items-center gap-2 px-3 py-2 bg-[#111] rounded border border-[#1a1a1a]">
1680
+ <span className={`w-2 h-2 rounded-full shrink-0 ${sub.enabled ? "bg-cyan-400" : "bg-[#444]"}`} />
1681
+ <span className={`text-sm flex-1 ${sub.enabled ? "text-cyan-400" : "text-[#666]"}`}>
1682
+ {sub.trigger_slug.replace(/_/g, " ")}
1683
+ </span>
1684
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${sub.enabled ? "bg-cyan-500/10 text-cyan-400" : "bg-[#222] text-[#555]"}`}>
1685
+ {sub.enabled ? "active" : "disabled"}
1686
+ </span>
1687
+ </div>
1688
+ ))}
1689
+ </div>
1690
+ )}
1691
+ </div>
1692
+
1446
1693
  {/* Developer Info (dev mode only) */}
1447
- {isDev && apiKey && (
1694
+ {apiKey && (
1448
1695
  <div className="mt-8 pt-6 border-t border-[#222]">
1449
1696
  <p className="text-sm text-[#666] mb-3">Developer Info</p>
1450
1697
  <div className="space-y-2">
@@ -1466,17 +1713,15 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1466
1713
  {showApiKey ? "Hide" : "Show"}
1467
1714
  </button>
1468
1715
  </div>
1469
- {showApiKey && (
1470
- <code className="text-xs bg-[#1a1a1a] px-2 py-1 rounded text-[#888] break-all">
1471
- {apiKey}
1472
- </code>
1473
- )}
1716
+ <code className="text-xs bg-[#1a1a1a] px-2 py-1 rounded text-[#888] break-all">
1717
+ {showApiKey ? (apiKeyFull || apiKey) : apiKey}
1718
+ </code>
1474
1719
  </div>
1475
1720
  {agent.status === "running" && agent.port && (
1476
1721
  <div className="flex flex-col gap-1 mt-2">
1477
1722
  <span className="text-xs text-[#666]">Test with curl</span>
1478
1723
  <code className="text-xs bg-[#1a1a1a] px-2 py-1.5 rounded text-[#666] break-all">
1479
- curl -H "X-API-Key: {showApiKey ? apiKey : "***"}" http://localhost:{agent.port}/config
1724
+ curl -H "X-API-Key: {showApiKey ? (apiKeyFull || apiKey) : "***"}" http://localhost:{agent.port}/config
1480
1725
  </code>
1481
1726
  </div>
1482
1727
  )}
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState, useEffect, useCallback } from "react";
2
2
  import { useAuth, useProjects } from "../../context";
3
3
  import { IntegrationsPanel } from "../mcp/IntegrationsPanel";
4
4
 
@@ -12,18 +12,35 @@ interface TriggerType {
12
12
  logo: string | null;
13
13
  }
14
14
 
15
+ interface ProviderInfo {
16
+ id: string;
17
+ name: string;
18
+ connected: boolean;
19
+ }
20
+
15
21
  export function IntegrationsTab() {
16
22
  const { authFetch } = useAuth();
17
23
  const { currentProjectId } = useProjects();
18
24
 
19
25
  const projectId = currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null;
26
+ const projectParam = projectId ? `?project_id=${projectId}` : "";
20
27
 
21
- // Provider selection
22
- const [selectedProvider, setSelectedProvider] = useState("composio");
23
- const providerOptions = [
24
- { id: "composio", name: "Composio" },
25
- { id: "agentdojo", name: "AgentDojo" },
26
- ];
28
+ // Provider selection — only show configured providers
29
+ const [providers, setProviders] = useState<ProviderInfo[]>([]);
30
+ const [selectedProvider, setSelectedProvider] = useState("");
31
+
32
+ useEffect(() => {
33
+ authFetch(`/api/triggers/providers${projectParam}`)
34
+ .then(r => r.json())
35
+ .then(data => {
36
+ const connected = (data.providers || []).filter((p: ProviderInfo) => p.connected);
37
+ setProviders(connected);
38
+ if (connected.length > 0 && !connected.find((p: ProviderInfo) => p.id === selectedProvider)) {
39
+ setSelectedProvider(connected[0].id);
40
+ }
41
+ })
42
+ .catch(() => {});
43
+ }, [authFetch]);
27
44
 
28
45
  // Trigger type browsing
29
46
  const [browsingToolkit, setBrowsingToolkit] = useState<string | null>(null);
@@ -53,32 +70,41 @@ export function IntegrationsTab() {
53
70
  Connect external apps via OAuth or API Key. Connected apps can be used for triggers and MCP integrations.
54
71
  </p>
55
72
 
56
- {/* Provider Selector */}
57
- <div className="flex items-center gap-2 mb-4">
58
- <span className="text-xs text-[#666]">Provider:</span>
59
- <div className="flex gap-1 bg-[#111] border border-[#1a1a1a] rounded-lg p-0.5">
60
- {providerOptions.map(p => (
61
- <button
62
- key={p.id}
63
- onClick={() => setSelectedProvider(p.id)}
64
- className={`px-3 py-1 rounded text-xs font-medium transition ${
65
- selectedProvider === p.id
66
- ? "bg-[#1a1a1a] text-white"
67
- : "text-[#666] hover:text-[#888]"
68
- }`}
69
- >
70
- {p.name}
71
- </button>
72
- ))}
73
+ {/* Provider Selector — only show if multiple configured */}
74
+ {providers.length > 1 && (
75
+ <div className="flex items-center gap-2 mb-4">
76
+ <span className="text-xs text-[#666]">Provider:</span>
77
+ <div className="flex gap-1 bg-[#111] border border-[#1a1a1a] rounded-lg p-0.5">
78
+ {providers.map(p => (
79
+ <button
80
+ key={p.id}
81
+ onClick={() => setSelectedProvider(p.id)}
82
+ className={`px-3 py-1 rounded text-xs font-medium transition ${
83
+ selectedProvider === p.id
84
+ ? "bg-[#1a1a1a] text-white"
85
+ : "text-[#666] hover:text-[#888]"
86
+ }`}
87
+ >
88
+ {p.name}
89
+ </button>
90
+ ))}
91
+ </div>
73
92
  </div>
74
- </div>
93
+ )}
75
94
 
76
- <IntegrationsPanel
77
- providerId={selectedProvider}
78
- projectId={projectId}
79
- hideMcpConfig
80
- onBrowseTriggers={handleBrowseTriggers}
81
- />
95
+ {providers.length === 0 ? (
96
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-8 text-center">
97
+ <p className="text-[#666]">No integration providers configured.</p>
98
+ <p className="text-sm text-[#555] mt-1">Add API keys for Composio or AgentDojo in Settings.</p>
99
+ </div>
100
+ ) : (
101
+ <IntegrationsPanel
102
+ providerId={selectedProvider}
103
+ projectId={projectId}
104
+ hideMcpConfig
105
+ onBrowseTriggers={handleBrowseTriggers}
106
+ />
107
+ )}
82
108
 
83
109
  {/* Trigger Types Panel */}
84
110
  {browsingToolkit && (