apteva 0.4.17 → 0.4.18

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 (59) hide show
  1. package/dist/ActivityPage.yv28a2vj.js +3 -0
  2. package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
  3. package/dist/App.155wke5v.js +4 -0
  4. package/dist/App.2e19nvn4.js +13 -0
  5. package/dist/App.2ye1b5n0.js +4 -0
  6. package/dist/App.4da4ycbe.js +4 -0
  7. package/dist/App.b6wtzd1j.js +4 -0
  8. package/dist/App.fjrh28tf.js +4 -0
  9. package/dist/App.htc36cy8.js +4 -0
  10. package/dist/App.me6reaa6.js +4 -0
  11. package/dist/App.n5q6p960.js +4 -0
  12. package/dist/App.nft7h9jt.js +4 -0
  13. package/dist/App.np463xvy.js +4 -0
  14. package/dist/App.nps62kvt.js +4 -0
  15. package/dist/App.q8ws33cc.js +181 -0
  16. package/dist/App.tb0y0jmt.js +40 -0
  17. package/dist/ConnectionsPage.52evzrp7.js +3 -0
  18. package/dist/McpPage.bjqrp0n2.js +3 -0
  19. package/dist/SettingsPage.es76hnj2.js +3 -0
  20. package/dist/SkillsPage.06h8yf0h.js +3 -0
  21. package/dist/TasksPage.99df66mk.js +3 -0
  22. package/dist/TelemetryPage.bmdnxhq7.js +3 -0
  23. package/dist/TestsPage.denxrg8c.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/middleware.ts +2 -0
  28. package/src/db.ts +162 -11
  29. package/src/mcp-platform.ts +41 -1
  30. package/src/routes/api/agent-utils.ts +38 -2
  31. package/src/routes/api/agents.ts +65 -2
  32. package/src/routes/api/projects.ts +19 -2
  33. package/src/routes/api/system.ts +26 -12
  34. package/src/routes/api/triggers.ts +458 -0
  35. package/src/routes/api/webhooks.ts +171 -0
  36. package/src/routes/api.ts +4 -0
  37. package/src/routes/static.ts +12 -3
  38. package/src/server.ts +4 -2
  39. package/src/triggers/agentdojo.ts +248 -0
  40. package/src/triggers/composio.ts +264 -0
  41. package/src/triggers/index.ts +71 -0
  42. package/src/web/App.tsx +17 -10
  43. package/src/web/components/agents/AgentCard.tsx +14 -7
  44. package/src/web/components/agents/AgentPanel.tsx +105 -115
  45. package/src/web/components/common/Icons.tsx +8 -0
  46. package/src/web/components/common/index.ts +1 -0
  47. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  48. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  49. package/src/web/components/connections/OverviewTab.tsx +183 -0
  50. package/src/web/components/connections/TriggersTab.tsx +690 -0
  51. package/src/web/components/index.ts +1 -0
  52. package/src/web/components/layout/Sidebar.tsx +7 -1
  53. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  54. package/src/web/components/settings/SettingsPage.tsx +96 -2
  55. package/src/web/components/tasks/TasksPage.tsx +2 -2
  56. package/src/web/components/tests/TestsPage.tsx +1 -2
  57. package/src/web/hooks/useAgents.ts +15 -11
  58. package/src/web/types.ts +1 -1
  59. package/dist/App.fq4xbpcz.js +0 -228
@@ -0,0 +1,690 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { useAuth, useProjects } from "../../context";
3
+ import { Select } from "../common";
4
+
5
+ interface TriggerType {
6
+ slug: string;
7
+ name: string;
8
+ description: string;
9
+ type: "webhook" | "poll";
10
+ toolkit_slug: string;
11
+ toolkit_name: string;
12
+ logo: string | null;
13
+ config_schema: Record<string, unknown>;
14
+ payload_schema: Record<string, unknown>;
15
+ }
16
+
17
+ interface TriggerInstance {
18
+ id: string;
19
+ trigger_slug: string;
20
+ connected_account_id: string | null;
21
+ status: "active" | "disabled";
22
+ config: Record<string, unknown>;
23
+ created_at: string;
24
+ }
25
+
26
+ interface Subscription {
27
+ id: string;
28
+ trigger_slug: string;
29
+ trigger_instance_id: string | null;
30
+ agent_id: string;
31
+ enabled: boolean;
32
+ project_id: string | null;
33
+ created_at: string;
34
+ updated_at: string;
35
+ }
36
+
37
+ interface ConnectedAccount {
38
+ id: string;
39
+ appId: string;
40
+ appName: string;
41
+ status: string;
42
+ }
43
+
44
+ interface Agent {
45
+ id: string;
46
+ name: string;
47
+ status: string;
48
+ port: number | null;
49
+ }
50
+
51
+ interface TriggerProviderInfo {
52
+ id: string;
53
+ name: string;
54
+ connected: boolean;
55
+ }
56
+
57
+ export function TriggersTab() {
58
+ const { authFetch } = useAuth();
59
+ const { currentProjectId } = useProjects();
60
+
61
+ // Provider selection
62
+ const [providers, setProviders] = useState<TriggerProviderInfo[]>([]);
63
+ const [selectedProvider, setSelectedProvider] = useState("composio");
64
+
65
+ // Trigger instances (from selected provider)
66
+ const [triggers, setTriggers] = useState<TriggerInstance[]>([]);
67
+ const [triggersLoading, setTriggersLoading] = useState(true);
68
+
69
+ // Subscriptions (local routing)
70
+ const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
71
+
72
+ // Browse trigger types
73
+ const [triggerTypes, setTriggerTypes] = useState<TriggerType[]>([]);
74
+ const [typesLoading, setTypesLoading] = useState(false);
75
+ const [toolkitFilter, setToolkitFilter] = useState("");
76
+ const [typeSearch, setTypeSearch] = useState("");
77
+
78
+ // Create trigger
79
+ const [showCreate, setShowCreate] = useState(false);
80
+ const [selectedType, setSelectedType] = useState<TriggerType | null>(null);
81
+ const [connectedAccounts, setConnectedAccounts] = useState<ConnectedAccount[]>([]);
82
+ const [selectedAccountId, setSelectedAccountId] = useState("");
83
+ const [creating, setCreating] = useState(false);
84
+
85
+ // Add subscription
86
+ const [showAddSub, setShowAddSub] = useState(false);
87
+ const [subTriggerId, setSubTriggerId] = useState("");
88
+ const [subAgentId, setSubAgentId] = useState("");
89
+ const [addingSub, setAddingSub] = useState(false);
90
+
91
+ // Agents
92
+ const [agents, setAgents] = useState<Agent[]>([]);
93
+
94
+ const [error, setError] = useState<string | null>(null);
95
+
96
+ const projectParam = currentProjectId && currentProjectId !== "unassigned" ? `?project_id=${currentProjectId}` : "";
97
+
98
+ // Fetch available providers
99
+ const fetchProviders = useCallback(async () => {
100
+ try {
101
+ const res = await authFetch(`/api/triggers/providers`);
102
+ if (res.ok) {
103
+ const data = await res.json();
104
+ setProviders(data.providers || []);
105
+ }
106
+ } catch (e) {
107
+ console.error("Failed to fetch providers:", e);
108
+ }
109
+ }, [authFetch]);
110
+
111
+ // Fetch active triggers
112
+ const fetchTriggers = useCallback(async () => {
113
+ setTriggersLoading(true);
114
+ try {
115
+ const providerParam = `provider=${selectedProvider}`;
116
+ const sep = projectParam ? "&" : "?";
117
+ const url = projectParam
118
+ ? `/api/triggers${projectParam}&${providerParam}`
119
+ : `/api/triggers?${providerParam}`;
120
+ const res = await authFetch(url);
121
+ if (res.ok) {
122
+ const data = await res.json();
123
+ setTriggers(data.triggers || []);
124
+ }
125
+ } catch (e) {
126
+ console.error("Failed to fetch triggers:", e);
127
+ }
128
+ setTriggersLoading(false);
129
+ }, [authFetch, projectParam, selectedProvider]);
130
+
131
+ // Fetch subscriptions
132
+ const fetchSubscriptions = useCallback(async () => {
133
+ try {
134
+ const res = await authFetch(`/api/subscriptions${projectParam}`);
135
+ if (res.ok) {
136
+ const data = await res.json();
137
+ setSubscriptions(data.subscriptions || []);
138
+ }
139
+ } catch (e) {
140
+ console.error("Failed to fetch subscriptions:", e);
141
+ }
142
+ }, [authFetch, projectParam]);
143
+
144
+ // Fetch agents
145
+ const fetchAgents = useCallback(async () => {
146
+ try {
147
+ const res = await authFetch(`/api/agents`);
148
+ if (res.ok) {
149
+ const data = await res.json();
150
+ setAgents(data.agents || []);
151
+ }
152
+ } catch (e) {
153
+ // Ignore
154
+ }
155
+ }, [authFetch]);
156
+
157
+ useEffect(() => {
158
+ fetchProviders();
159
+ fetchTriggers();
160
+ fetchSubscriptions();
161
+ fetchAgents();
162
+ }, [fetchProviders, fetchTriggers, fetchSubscriptions, fetchAgents]);
163
+
164
+ // Browse trigger types
165
+ const browseTriggerTypes = async (toolkit?: string) => {
166
+ setTypesLoading(true);
167
+ try {
168
+ let url = `/api/triggers/types?provider=${selectedProvider}`;
169
+ if (toolkit) url += `&toolkit_slugs=${toolkit}`;
170
+ if (currentProjectId && currentProjectId !== "unassigned") url += `&project_id=${currentProjectId}`;
171
+ const res = await authFetch(url);
172
+ if (res.ok) {
173
+ const data = await res.json();
174
+ setTriggerTypes(data.types || []);
175
+ } else {
176
+ const data = await res.json();
177
+ setError(data.error || "Failed to fetch trigger types");
178
+ }
179
+ } catch (e) {
180
+ setError("Failed to fetch trigger types");
181
+ }
182
+ setTypesLoading(false);
183
+ };
184
+
185
+ // Fetch connected accounts when creating
186
+ const fetchConnectedAccounts = async () => {
187
+ try {
188
+ const res = await authFetch(`/api/integrations/${selectedProvider}/connected${projectParam}`);
189
+ if (res.ok) {
190
+ const data = await res.json();
191
+ setConnectedAccounts((data.accounts || []).filter((a: ConnectedAccount) => a.status === "active"));
192
+ }
193
+ } catch (e) {
194
+ // Ignore
195
+ }
196
+ };
197
+
198
+ // Start create flow
199
+ const startCreate = (triggerType: TriggerType) => {
200
+ setSelectedType(triggerType);
201
+ setSelectedAccountId("");
202
+ setShowCreate(true);
203
+ fetchConnectedAccounts();
204
+ };
205
+
206
+ // Create trigger
207
+ const handleCreate = async () => {
208
+ if (!selectedType || !selectedAccountId) return;
209
+
210
+ setCreating(true);
211
+ setError(null);
212
+ try {
213
+ const providerParam = `provider=${selectedProvider}`;
214
+ const sep = projectParam ? "&" : "?";
215
+ const url = projectParam
216
+ ? `/api/triggers${projectParam}&${providerParam}`
217
+ : `/api/triggers?${providerParam}`;
218
+ const res = await authFetch(url, {
219
+ method: "POST",
220
+ headers: { "Content-Type": "application/json" },
221
+ body: JSON.stringify({
222
+ slug: selectedType.slug,
223
+ connectedAccountId: selectedAccountId,
224
+ }),
225
+ });
226
+ const data = await res.json();
227
+ if (!res.ok) {
228
+ setError(data.error || "Failed to create trigger");
229
+ } else {
230
+ setShowCreate(false);
231
+ setSelectedType(null);
232
+ fetchTriggers();
233
+ }
234
+ } catch (e: any) {
235
+ setError(e.message || "Failed to create trigger");
236
+ }
237
+ setCreating(false);
238
+ };
239
+
240
+ // Enable/disable trigger
241
+ const toggleTrigger = async (triggerId: string, currentStatus: string) => {
242
+ const action = currentStatus === "active" ? "disable" : "enable";
243
+ try {
244
+ const providerQ = projectParam ? `&provider=${selectedProvider}` : `?provider=${selectedProvider}`;
245
+ const res = await authFetch(`/api/triggers/${triggerId}/${action}${projectParam}${providerQ}`, {
246
+ method: "POST",
247
+ });
248
+ if (res.ok) {
249
+ fetchTriggers();
250
+ } else {
251
+ const data = await res.json();
252
+ setError(data.error || `Failed to ${action} trigger`);
253
+ }
254
+ } catch (e) {
255
+ setError(`Failed to ${action} trigger`);
256
+ }
257
+ };
258
+
259
+ // Delete trigger
260
+ const deleteTrigger = async (triggerId: string) => {
261
+ try {
262
+ const providerQ = projectParam ? `&provider=${selectedProvider}` : `?provider=${selectedProvider}`;
263
+ const res = await authFetch(`/api/triggers/${triggerId}${projectParam}${providerQ}`, {
264
+ method: "DELETE",
265
+ });
266
+ if (res.ok) {
267
+ fetchTriggers();
268
+ } else {
269
+ const data = await res.json();
270
+ setError(data.error || "Failed to delete trigger");
271
+ }
272
+ } catch (e) {
273
+ setError("Failed to delete trigger");
274
+ }
275
+ };
276
+
277
+ // Add subscription — backend auto-creates webhook if needed
278
+ const handleAddSubscription = async () => {
279
+ if (!subTriggerId || !subAgentId) return;
280
+
281
+ // Find the trigger instance to get its slug
282
+ const trigger = triggers.find(t => t.id === subTriggerId);
283
+ if (!trigger) return;
284
+
285
+ setAddingSub(true);
286
+ setError(null);
287
+ try {
288
+ const res = await authFetch(`/api/subscriptions`, {
289
+ method: "POST",
290
+ headers: { "Content-Type": "application/json" },
291
+ body: JSON.stringify({
292
+ trigger_slug: trigger.trigger_slug,
293
+ trigger_instance_id: trigger.id,
294
+ agent_id: subAgentId,
295
+ provider: selectedProvider,
296
+ project_id: currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null,
297
+ public_url: window.location.origin,
298
+ }),
299
+ });
300
+ const data = await res.json();
301
+ if (!res.ok) {
302
+ setError(data.error || "Failed to create subscription");
303
+ } else {
304
+ setShowAddSub(false);
305
+ setSubTriggerId("");
306
+ setSubAgentId("");
307
+ fetchSubscriptions();
308
+ }
309
+ } catch (e: any) {
310
+ setError(e.message || "Failed to create subscription");
311
+ }
312
+ setAddingSub(false);
313
+ };
314
+
315
+ // Toggle subscription
316
+ const toggleSubscription = async (sub: Subscription) => {
317
+ const action = sub.enabled ? "disable" : "enable";
318
+ try {
319
+ const res = await authFetch(`/api/subscriptions/${sub.id}/${action}`, {
320
+ method: "POST",
321
+ });
322
+ if (res.ok) fetchSubscriptions();
323
+ } catch (e) {
324
+ setError(`Failed to ${action} subscription`);
325
+ }
326
+ };
327
+
328
+ // Delete subscription
329
+ const deleteSubscription = async (id: string) => {
330
+ try {
331
+ const res = await authFetch(`/api/subscriptions/${id}`, {
332
+ method: "DELETE",
333
+ });
334
+ if (res.ok) fetchSubscriptions();
335
+ } catch (e) {
336
+ setError("Failed to delete subscription");
337
+ }
338
+ };
339
+
340
+ // Filter trigger types by search
341
+ const filteredTypes = triggerTypes.filter(t => {
342
+ if (!typeSearch) return true;
343
+ const s = typeSearch.toLowerCase();
344
+ return t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) || t.description.toLowerCase().includes(s);
345
+ });
346
+
347
+ // Agent map for quick lookups
348
+ const agentMap = new Map(agents.map(a => [a.id, a]));
349
+
350
+ return (
351
+ <div className="space-y-6">
352
+ {/* Error */}
353
+ {error && (
354
+ <div className="text-red-400 text-sm p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center justify-between">
355
+ <span>{error}</span>
356
+ <button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">x</button>
357
+ </div>
358
+ )}
359
+
360
+ {/* Provider Selector */}
361
+ {providers.length > 1 && (
362
+ <div className="flex items-center gap-2">
363
+ <span className="text-xs text-[#666]">Provider:</span>
364
+ <div className="flex gap-1 bg-[#111] border border-[#1a1a1a] rounded-lg p-0.5">
365
+ {providers.map(p => (
366
+ <button
367
+ key={p.id}
368
+ onClick={() => {
369
+ setSelectedProvider(p.id);
370
+ setTriggerTypes([]);
371
+ setToolkitFilter("");
372
+ setTypeSearch("");
373
+ }}
374
+ className={`px-3 py-1 rounded text-xs font-medium transition ${
375
+ selectedProvider === p.id
376
+ ? "bg-[#1a1a1a] text-white"
377
+ : "text-[#666] hover:text-[#888]"
378
+ }`}
379
+ >
380
+ {p.name}
381
+ {!p.connected && (
382
+ <span className="ml-1 text-[10px] text-yellow-500" title="API key not configured">!</span>
383
+ )}
384
+ </button>
385
+ ))}
386
+ </div>
387
+ </div>
388
+ )}
389
+
390
+ {/* Subscriptions (trigger → agent routing) */}
391
+ <section>
392
+ <div className="flex items-center justify-between mb-3">
393
+ <h3 className="text-sm font-medium text-[#888]">
394
+ Subscriptions ({subscriptions.length})
395
+ </h3>
396
+ <button
397
+ onClick={() => setShowAddSub(true)}
398
+ className="text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-3 py-1.5 rounded transition"
399
+ >
400
+ + Add Subscription
401
+ </button>
402
+ </div>
403
+
404
+ {subscriptions.length === 0 ? (
405
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center text-[#666] text-sm">
406
+ No subscriptions yet. Add one to route trigger events to an agent.
407
+ </div>
408
+ ) : (
409
+ <div className="space-y-2">
410
+ {subscriptions.map(sub => {
411
+ const agent = agentMap.get(sub.agent_id);
412
+ return (
413
+ <div key={sub.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 flex items-center gap-3">
414
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${sub.enabled ? "bg-green-400" : "bg-[#666]"}`} />
415
+ <div className="flex-1 min-w-0">
416
+ <div className="text-sm font-medium truncate">
417
+ {sub.trigger_slug.replace(/_/g, " ")}
418
+ <span className="text-[#555] mx-1.5">&rarr;</span>
419
+ <span className="text-[#f97316]">{agent?.name || "Unknown Agent"}</span>
420
+ </div>
421
+ <div className="text-xs text-[#666]">
422
+ {sub.trigger_instance_id
423
+ ? `Instance: ${sub.trigger_instance_id.slice(0, 12)}...`
424
+ : "All instances"
425
+ }
426
+ </div>
427
+ </div>
428
+ <div className="flex items-center gap-2 flex-shrink-0">
429
+ <button
430
+ onClick={() => toggleSubscription(sub)}
431
+ className={`text-xs px-3 py-1 rounded transition ${
432
+ sub.enabled
433
+ ? "bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20"
434
+ : "bg-green-500/10 text-green-400 hover:bg-green-500/20"
435
+ }`}
436
+ >
437
+ {sub.enabled ? "Disable" : "Enable"}
438
+ </button>
439
+ <button
440
+ onClick={() => deleteSubscription(sub.id)}
441
+ className="text-xs text-[#666] hover:text-red-400 transition px-2"
442
+ >
443
+ Delete
444
+ </button>
445
+ </div>
446
+ </div>
447
+ );
448
+ })}
449
+ </div>
450
+ )}
451
+ </section>
452
+
453
+ {/* Trigger Instances */}
454
+ <section>
455
+ <h3 className="text-sm font-medium text-[#888] mb-3">
456
+ Trigger Instances ({triggers.length})
457
+ </h3>
458
+ {triggersLoading ? (
459
+ <div className="text-center py-6 text-[#666] text-sm">Loading triggers...</div>
460
+ ) : triggers.length === 0 ? (
461
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center text-[#666] text-sm">
462
+ No trigger instances. Browse trigger types below to create one.
463
+ </div>
464
+ ) : (
465
+ <div className="space-y-2">
466
+ {triggers.map(trigger => (
467
+ <div key={trigger.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 flex items-center gap-3">
468
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${trigger.status === "active" ? "bg-green-400" : "bg-[#666]"}`} />
469
+ <div className="flex-1 min-w-0">
470
+ <div className="text-sm font-medium truncate">
471
+ {trigger.trigger_slug.replace(/_/g, " ")}
472
+ </div>
473
+ <div className="text-xs text-[#666]">
474
+ ID: {trigger.id.slice(0, 12)}... | Created: {new Date(trigger.created_at).toLocaleDateString()}
475
+ </div>
476
+ </div>
477
+ <div className="flex items-center gap-2 flex-shrink-0">
478
+ <button
479
+ onClick={() => toggleTrigger(trigger.id, trigger.status)}
480
+ className={`text-xs px-3 py-1 rounded transition ${
481
+ trigger.status === "active"
482
+ ? "bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20"
483
+ : "bg-green-500/10 text-green-400 hover:bg-green-500/20"
484
+ }`}
485
+ >
486
+ {trigger.status === "active" ? "Disable" : "Enable"}
487
+ </button>
488
+ <button
489
+ onClick={() => deleteTrigger(trigger.id)}
490
+ className="text-xs text-[#666] hover:text-red-400 transition px-2"
491
+ >
492
+ Delete
493
+ </button>
494
+ </div>
495
+ </div>
496
+ ))}
497
+ </div>
498
+ )}
499
+ </section>
500
+
501
+ {/* Browse Trigger Types */}
502
+ <section>
503
+ <h3 className="text-sm font-medium text-[#888] mb-3">Browse Trigger Types</h3>
504
+ <div className="flex gap-2 mb-3">
505
+ <input
506
+ type="text"
507
+ value={toolkitFilter}
508
+ onChange={(e) => setToolkitFilter(e.target.value)}
509
+ placeholder="Toolkit filter (e.g. github, gmail, slack)"
510
+ className="flex-1 bg-[#111] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
511
+ />
512
+ <button
513
+ onClick={() => browseTriggerTypes(toolkitFilter || undefined)}
514
+ disabled={typesLoading}
515
+ className="text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-4 py-2 rounded transition disabled:opacity-50"
516
+ >
517
+ {typesLoading ? "Loading..." : "Browse"}
518
+ </button>
519
+ </div>
520
+
521
+ {triggerTypes.length > 0 && (
522
+ <>
523
+ <input
524
+ type="text"
525
+ value={typeSearch}
526
+ onChange={(e) => setTypeSearch(e.target.value)}
527
+ placeholder="Search trigger types..."
528
+ className="w-full bg-[#111] border border-[#333] rounded px-3 py-2 text-sm mb-3 focus:outline-none focus:border-[#f97316]"
529
+ />
530
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
531
+ {filteredTypes.slice(0, 30).map(tt => (
532
+ <div key={tt.slug} className="bg-[#111] border border-[#1a1a1a] hover:border-[#333] rounded-lg p-3 transition">
533
+ <div className="flex items-start gap-3">
534
+ {tt.logo ? (
535
+ <img src={tt.logo} alt={tt.toolkit_name} className="w-8 h-8 rounded object-contain flex-shrink-0" />
536
+ ) : (
537
+ <div className="w-8 h-8 rounded bg-[#1a1a1a] flex items-center justify-center text-xs flex-shrink-0">
538
+ {tt.toolkit_name?.[0]?.toUpperCase() || "?"}
539
+ </div>
540
+ )}
541
+ <div className="flex-1 min-w-0">
542
+ <div className="text-sm font-medium truncate">{tt.name}</div>
543
+ <div className="text-xs text-[#666]">{tt.toolkit_name}</div>
544
+ <div className="text-xs text-[#555] mt-1 line-clamp-2">{tt.description}</div>
545
+ </div>
546
+ </div>
547
+ <button
548
+ onClick={() => startCreate(tt)}
549
+ className="w-full mt-3 text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-3 py-1.5 rounded transition"
550
+ >
551
+ Create Trigger
552
+ </button>
553
+ </div>
554
+ ))}
555
+ </div>
556
+ {filteredTypes.length > 30 && (
557
+ <p className="text-xs text-[#555] mt-3 text-center">
558
+ Showing first 30 of {filteredTypes.length} types. Use search to filter.
559
+ </p>
560
+ )}
561
+ </>
562
+ )}
563
+ </section>
564
+
565
+ {/* Create Trigger Modal */}
566
+ {showCreate && selectedType && (
567
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
568
+ <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-md mx-4">
569
+ <h3 className="font-medium mb-1">Create Trigger</h3>
570
+ <p className="text-xs text-[#666] mb-4">{selectedType.name}</p>
571
+
572
+ <div className="space-y-4">
573
+ <div>
574
+ <label className="block text-xs text-[#888] mb-1.5">Connected Account</label>
575
+ {connectedAccounts.length === 0 ? (
576
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
577
+ No connected accounts available. Connect an app first in the Integrations tab.
578
+ </div>
579
+ ) : (
580
+ <Select
581
+ value={selectedAccountId}
582
+ onChange={setSelectedAccountId}
583
+ placeholder="Select account..."
584
+ options={connectedAccounts.map(acc => ({
585
+ value: acc.id,
586
+ label: `${acc.appName} (${acc.id.slice(0, 8)}...)`,
587
+ }))}
588
+ />
589
+ )}
590
+ </div>
591
+ </div>
592
+
593
+ <div className="flex gap-2 mt-4">
594
+ <button
595
+ onClick={() => { setShowCreate(false); setSelectedType(null); }}
596
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
597
+ >
598
+ Cancel
599
+ </button>
600
+ <button
601
+ onClick={handleCreate}
602
+ disabled={!selectedAccountId || creating}
603
+ className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
604
+ >
605
+ {creating ? "Creating..." : "Create"}
606
+ </button>
607
+ </div>
608
+ </div>
609
+ </div>
610
+ )}
611
+
612
+ {/* Add Subscription Modal */}
613
+ {showAddSub && (
614
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
615
+ <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-md mx-4">
616
+ <h3 className="font-medium mb-1">Route Trigger to Agent</h3>
617
+ <p className="text-xs text-[#666] mb-4">
618
+ {triggers.length === 0
619
+ ? "No trigger instances yet. Create one first from the Browse section below."
620
+ : "Select a trigger instance and the agent that should handle its events."
621
+ }
622
+ </p>
623
+
624
+ {triggers.length > 0 ? (
625
+ <>
626
+ <div className="space-y-4">
627
+ <div>
628
+ <label className="block text-xs text-[#888] mb-1.5">Trigger Instance</label>
629
+ <Select
630
+ value={subTriggerId}
631
+ onChange={setSubTriggerId}
632
+ placeholder="Select trigger..."
633
+ options={triggers.map(t => ({
634
+ value: t.id,
635
+ label: `${t.trigger_slug.replace(/_/g, " ")}`,
636
+ }))}
637
+ />
638
+ {subTriggerId && (
639
+ <div className="text-xs text-[#555] mt-1 font-mono">
640
+ ID: {subTriggerId.slice(0, 16)}...
641
+ </div>
642
+ )}
643
+ </div>
644
+
645
+ <div>
646
+ <label className="block text-xs text-[#888] mb-1.5">Target Agent</label>
647
+ <Select
648
+ value={subAgentId}
649
+ onChange={setSubAgentId}
650
+ placeholder="Select agent..."
651
+ options={agents.map(agent => ({
652
+ value: agent.id,
653
+ label: `${agent.name} (${agent.status})`,
654
+ }))}
655
+ />
656
+ </div>
657
+ </div>
658
+
659
+ <div className="flex gap-2 mt-5">
660
+ <button
661
+ onClick={() => { setShowAddSub(false); setSubTriggerId(""); setSubAgentId(""); }}
662
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
663
+ >
664
+ Cancel
665
+ </button>
666
+ <button
667
+ onClick={handleAddSubscription}
668
+ disabled={!subTriggerId || !subAgentId || addingSub}
669
+ className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
670
+ >
671
+ {addingSub ? "Adding..." : "Add"}
672
+ </button>
673
+ </div>
674
+ </>
675
+ ) : (
676
+ <div className="flex gap-2 mt-4">
677
+ <button
678
+ onClick={() => setShowAddSub(false)}
679
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
680
+ >
681
+ Close
682
+ </button>
683
+ </div>
684
+ )}
685
+ </div>
686
+ </div>
687
+ )}
688
+ </div>
689
+ );
690
+ }
@@ -18,3 +18,4 @@ export { McpPage } from "./mcp";
18
18
  export { SkillsPage } from "./skills/SkillsPage";
19
19
  export { TestsPage } from "./tests/TestsPage";
20
20
  export { TelemetryPage } from "./telemetry/TelemetryPage";
21
+ export { ConnectionsPage } from "./connections/ConnectionsPage";