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,1169 @@
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
+ const [createAgentId, setCreateAgentId] = useState(""); // For AgentDojo direct subscription flow
85
+ const [browseConfig, setBrowseConfig] = useState<Record<string, string>>({});
86
+
87
+ // AgentDojo add subscription modal — uses composio as data source (same as Integrations tab)
88
+ const [showAddDojo, setShowAddDojo] = useState(false);
89
+ const [dojoTriggerTypes, setDojoTriggerTypes] = useState<TriggerType[]>([]);
90
+ const [dojoTypesLoading, setDojoTypesLoading] = useState(false);
91
+ const [dojoAccounts, setDojoAccounts] = useState<ConnectedAccount[]>([]);
92
+ const [dojoSelectedType, setDojoSelectedType] = useState<string>("");
93
+ const [dojoAgentId, setDojoAgentId] = useState("");
94
+ const [dojoCreating, setDojoCreating] = useState(false);
95
+ const [dojoTypeSearch, setDojoTypeSearch] = useState("");
96
+ const [dojoConfig, setDojoConfig] = useState<Record<string, string>>({});
97
+
98
+ // Add subscription
99
+ const [showAddSub, setShowAddSub] = useState(false);
100
+ const [subTriggerId, setSubTriggerId] = useState("");
101
+ const [subAgentId, setSubAgentId] = useState("");
102
+ const [addingSub, setAddingSub] = useState(false);
103
+
104
+ // Agents
105
+ const [agents, setAgents] = useState<Agent[]>([]);
106
+
107
+ const [error, setError] = useState<string | null>(null);
108
+
109
+ const projectParam = currentProjectId && currentProjectId !== "unassigned" ? `?project_id=${currentProjectId}` : "";
110
+
111
+ // Fetch available providers
112
+ const fetchProviders = useCallback(async () => {
113
+ try {
114
+ const res = await authFetch(`/api/triggers/providers${projectParam}`);
115
+ if (res.ok) {
116
+ const data = await res.json();
117
+ setProviders(data.providers || []);
118
+ }
119
+ } catch (e) {
120
+ console.error("Failed to fetch providers:", e);
121
+ }
122
+ }, [authFetch]);
123
+
124
+ // Fetch active triggers
125
+ const fetchTriggers = useCallback(async () => {
126
+ setTriggersLoading(true);
127
+ try {
128
+ const providerParam = `provider=${selectedProvider}`;
129
+ const sep = projectParam ? "&" : "?";
130
+ const url = projectParam
131
+ ? `/api/triggers${projectParam}&${providerParam}`
132
+ : `/api/triggers?${providerParam}`;
133
+ const res = await authFetch(url);
134
+ if (res.ok) {
135
+ const data = await res.json();
136
+ setTriggers(data.triggers || []);
137
+ }
138
+ } catch (e) {
139
+ console.error("Failed to fetch triggers:", e);
140
+ }
141
+ setTriggersLoading(false);
142
+ }, [authFetch, projectParam, selectedProvider]);
143
+
144
+ // Fetch subscriptions
145
+ const fetchSubscriptions = useCallback(async () => {
146
+ try {
147
+ const res = await authFetch(`/api/subscriptions${projectParam}`);
148
+ if (res.ok) {
149
+ const data = await res.json();
150
+ setSubscriptions(data.subscriptions || []);
151
+ }
152
+ } catch (e) {
153
+ console.error("Failed to fetch subscriptions:", e);
154
+ }
155
+ }, [authFetch, projectParam]);
156
+
157
+ // Fetch agents
158
+ const fetchAgents = useCallback(async () => {
159
+ try {
160
+ const res = await authFetch(`/api/agents`);
161
+ if (res.ok) {
162
+ const data = await res.json();
163
+ setAgents(data.agents || []);
164
+ }
165
+ } catch (e) {
166
+ // Ignore
167
+ }
168
+ }, [authFetch]);
169
+
170
+ useEffect(() => {
171
+ fetchProviders();
172
+ fetchTriggers();
173
+ fetchSubscriptions();
174
+ fetchAgents();
175
+ }, [fetchProviders, fetchTriggers, fetchSubscriptions, fetchAgents]);
176
+
177
+ // Browse trigger types
178
+ const browseTriggerTypes = async (toolkit?: string) => {
179
+ setTypesLoading(true);
180
+ try {
181
+ let url = `/api/triggers/types?provider=${selectedProvider}`;
182
+ if (toolkit) url += `&toolkit_slugs=${toolkit}`;
183
+ if (currentProjectId && currentProjectId !== "unassigned") url += `&project_id=${currentProjectId}`;
184
+ const res = await authFetch(url);
185
+ if (res.ok) {
186
+ const data = await res.json();
187
+ setTriggerTypes(data.types || []);
188
+ } else {
189
+ const data = await res.json();
190
+ setError(data.error || "Failed to fetch trigger types");
191
+ }
192
+ } catch (e) {
193
+ setError("Failed to fetch trigger types");
194
+ }
195
+ setTypesLoading(false);
196
+ };
197
+
198
+ // Fetch connected accounts when creating
199
+ const fetchConnectedAccounts = async () => {
200
+ try {
201
+ const res = await authFetch(`/api/integrations/${selectedProvider}/connected${projectParam}`);
202
+ if (res.ok) {
203
+ const data = await res.json();
204
+ setConnectedAccounts((data.accounts || []).filter((a: ConnectedAccount) => a.status === "active"));
205
+ }
206
+ } catch (e) {
207
+ // Ignore
208
+ }
209
+ };
210
+
211
+ // Start create flow
212
+ const startCreate = (triggerType: TriggerType) => {
213
+ setSelectedType(triggerType);
214
+ setSelectedAccountId("");
215
+ setCreateAgentId("");
216
+ setBrowseConfig({});
217
+ setShowCreate(true);
218
+ fetchConnectedAccounts();
219
+ };
220
+
221
+ const isAgentDojo = selectedProvider === "agentdojo";
222
+
223
+ // Open AgentDojo add subscription modal — fetches from agentdojo provider (same as Integrations tab)
224
+ const openAddDojoSub = async () => {
225
+ setShowAddDojo(true);
226
+ setDojoSelectedType("");
227
+ setDojoAgentId("");
228
+ setDojoTypeSearch("");
229
+ setDojoConfig({});
230
+
231
+ console.log("[openAddDojoSub] Opening modal, selectedProvider:", selectedProvider);
232
+ console.log("[openAddDojoSub] currentProjectId:", currentProjectId, "projectParam:", projectParam);
233
+ console.log("[openAddDojoSub] existing dojoTriggerTypes:", dojoTriggerTypes.length, "dojoAccounts:", dojoAccounts.length);
234
+
235
+ // Fetch trigger types from agentdojo (same provider as Integrations tab uses)
236
+ const loadTypes = async () => {
237
+ if (dojoTriggerTypes.length > 0) {
238
+ console.log("[openAddDojoSub] Already have", dojoTriggerTypes.length, "trigger types, skipping fetch");
239
+ return;
240
+ }
241
+ setDojoTypesLoading(true);
242
+ try {
243
+ let url = `/api/triggers/types?provider=agentdojo`;
244
+ if (currentProjectId && currentProjectId !== "unassigned") url += `&project_id=${currentProjectId}`;
245
+ console.log("[openAddDojoSub] Fetching trigger types:", url);
246
+ const res = await authFetch(url);
247
+ const data = await res.json();
248
+ console.log("[openAddDojoSub] Trigger types response:", res.status, "ok:", res.ok, "types count:", (data.types || []).length, "error:", data.error);
249
+ if (data.types && data.types.length > 0) {
250
+ console.log("[openAddDojoSub] Sample trigger type:", JSON.stringify(data.types[0]));
251
+ }
252
+ setDojoTriggerTypes(data.types || []);
253
+ } catch (e) {
254
+ console.error("[openAddDojoSub] Failed to load trigger types:", e);
255
+ }
256
+ setDojoTypesLoading(false);
257
+ };
258
+
259
+ // Fetch connected accounts from agentdojo (same as Integrations tab)
260
+ const loadAccounts = async () => {
261
+ try {
262
+ const url = `/api/integrations/agentdojo/connected${projectParam}`;
263
+ console.log("[openAddDojoSub] Fetching connected accounts:", url);
264
+ const res = await authFetch(url);
265
+ const data = await res.json();
266
+ console.log("[openAddDojoSub] Connected accounts response:", res.status, "ok:", res.ok, "accounts count:", (data.accounts || []).length, "error:", data.error);
267
+ if (data.accounts && data.accounts.length > 0) {
268
+ console.log("[openAddDojoSub] Sample account:", JSON.stringify(data.accounts[0]));
269
+ }
270
+ const active = (data.accounts || []).filter((a: ConnectedAccount) => a.status === "active");
271
+ console.log("[openAddDojoSub] Active accounts:", active.length);
272
+ setDojoAccounts(active);
273
+ } catch (e) {
274
+ console.error("[openAddDojoSub] Failed to load connected accounts:", e);
275
+ }
276
+ };
277
+
278
+ await Promise.all([loadTypes(), loadAccounts()]);
279
+ };
280
+
281
+ // Create AgentDojo subscription from the add-subscription modal
282
+ const handleAddDojoSub = async () => {
283
+ const tt = dojoTriggerTypes.find(t => t.slug === dojoSelectedType);
284
+ const matched = tt ? matchAccount(dojoAccounts, tt.toolkit_slug) : null;
285
+ console.log("[handleAddDojoSub] tt:", tt?.slug, "matched:", matched?.id, matched?.appName, "agentId:", dojoAgentId);
286
+ if (!tt || !dojoAgentId || !matched) return;
287
+
288
+ setDojoCreating(true);
289
+ setError(null);
290
+ try {
291
+ const agent = agents.find(a => a.id === dojoAgentId);
292
+ const providerParam = `provider=agentdojo`;
293
+ const url = projectParam
294
+ ? `/api/triggers${projectParam}&${providerParam}`
295
+ : `/api/triggers?${providerParam}`;
296
+ const configPayload = {
297
+ callback_url: `${window.location.origin}/api/webhooks/agentdojo`,
298
+ title: `${tt.name} → ${agent?.name || "Agent"}`,
299
+ server: tt.toolkit_slug,
300
+ agent_id: dojoAgentId,
301
+ ...dojoConfig, // Merge dynamic config fields (e.g. owner, repo for GitHub)
302
+ };
303
+ console.log("[handleAddDojoSub] config:", JSON.stringify(configPayload));
304
+ const res = await authFetch(url, {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify({
308
+ slug: tt.slug,
309
+ connectedAccountId: matched.id,
310
+ config: configPayload,
311
+ }),
312
+ });
313
+ const data = await res.json();
314
+ if (!res.ok) {
315
+ setError(data.error || "Failed to create subscription");
316
+ } else {
317
+ setShowAddDojo(false);
318
+ fetchTriggers();
319
+ }
320
+ } catch (e: any) {
321
+ setError(e.message || "Failed to create subscription");
322
+ }
323
+ setDojoCreating(false);
324
+ };
325
+
326
+ // Create trigger (Composio: trigger instance, AgentDojo: subscription + agent routing)
327
+ const handleCreate = async () => {
328
+ if (!selectedType) return;
329
+
330
+ // AgentDojo: create remote subscription directly (callback_url points to apteva webhook handler)
331
+ if (isAgentDojo) {
332
+ if (!createAgentId || !browseMatchedAccount) return;
333
+ setCreating(true);
334
+ setError(null);
335
+ try {
336
+ const agent = agents.find(a => a.id === createAgentId);
337
+ const instanceUrl = window.location.origin;
338
+ const providerParam = `provider=${selectedProvider}`;
339
+ const url = projectParam
340
+ ? `/api/triggers${projectParam}&${providerParam}`
341
+ : `/api/triggers?${providerParam}`;
342
+ const res = await authFetch(url, {
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/json" },
345
+ body: JSON.stringify({
346
+ slug: selectedType.slug,
347
+ connectedAccountId: browseMatchedAccount.id,
348
+ config: {
349
+ callback_url: `${instanceUrl}/api/webhooks/agentdojo`,
350
+ title: `${selectedType.name} → ${agent?.name || "Agent"}`,
351
+ server: selectedType.toolkit_slug,
352
+ agent_id: createAgentId,
353
+ ...browseConfig, // Dynamic config fields (e.g. owner, repo)
354
+ },
355
+ }),
356
+ });
357
+ const data = await res.json();
358
+ if (!res.ok) {
359
+ setError(data.error || "Failed to create subscription");
360
+ } else {
361
+ setShowCreate(false);
362
+ setSelectedType(null);
363
+ fetchTriggers();
364
+ }
365
+ } catch (e: any) {
366
+ setError(e.message || "Failed to create subscription");
367
+ }
368
+ setCreating(false);
369
+ return;
370
+ }
371
+
372
+ // Composio: standard trigger instance creation
373
+ if (!selectedAccountId) return;
374
+ setCreating(true);
375
+ setError(null);
376
+ try {
377
+ const providerParam = `provider=${selectedProvider}`;
378
+ const url = projectParam
379
+ ? `/api/triggers${projectParam}&${providerParam}`
380
+ : `/api/triggers?${providerParam}`;
381
+ const res = await authFetch(url, {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: JSON.stringify({
385
+ slug: selectedType.slug,
386
+ connectedAccountId: selectedAccountId,
387
+ }),
388
+ });
389
+ const data = await res.json();
390
+ if (!res.ok) {
391
+ setError(data.error || "Failed to create trigger");
392
+ } else {
393
+ setShowCreate(false);
394
+ setSelectedType(null);
395
+ fetchTriggers();
396
+ }
397
+ } catch (e: any) {
398
+ setError(e.message || "Failed to create trigger");
399
+ }
400
+ setCreating(false);
401
+ };
402
+
403
+ // Enable/disable trigger
404
+ const toggleTrigger = async (triggerId: string, currentStatus: string) => {
405
+ const action = currentStatus === "active" ? "disable" : "enable";
406
+ try {
407
+ const providerQ = projectParam ? `&provider=${selectedProvider}` : `?provider=${selectedProvider}`;
408
+ const res = await authFetch(`/api/triggers/${triggerId}/${action}${projectParam}${providerQ}`, {
409
+ method: "POST",
410
+ });
411
+ if (res.ok) {
412
+ fetchTriggers();
413
+ } else {
414
+ const data = await res.json();
415
+ setError(data.error || `Failed to ${action} trigger`);
416
+ }
417
+ } catch (e) {
418
+ setError(`Failed to ${action} trigger`);
419
+ }
420
+ };
421
+
422
+ // Delete trigger
423
+ const deleteTrigger = async (triggerId: string) => {
424
+ try {
425
+ const providerQ = projectParam ? `&provider=${selectedProvider}` : `?provider=${selectedProvider}`;
426
+ const res = await authFetch(`/api/triggers/${triggerId}${projectParam}${providerQ}`, {
427
+ method: "DELETE",
428
+ });
429
+ if (res.ok) {
430
+ fetchTriggers();
431
+ } else {
432
+ const data = await res.json();
433
+ setError(data.error || "Failed to delete trigger");
434
+ }
435
+ } catch (e) {
436
+ setError("Failed to delete trigger");
437
+ }
438
+ };
439
+
440
+ // Add subscription — backend auto-creates webhook if needed
441
+ const handleAddSubscription = async () => {
442
+ if (!subTriggerId || !subAgentId) return;
443
+
444
+ // Find the trigger instance to get its slug
445
+ const trigger = triggers.find(t => t.id === subTriggerId);
446
+ if (!trigger) return;
447
+
448
+ setAddingSub(true);
449
+ setError(null);
450
+ try {
451
+ const res = await authFetch(`/api/subscriptions`, {
452
+ method: "POST",
453
+ headers: { "Content-Type": "application/json" },
454
+ body: JSON.stringify({
455
+ trigger_slug: trigger.trigger_slug,
456
+ trigger_instance_id: trigger.id,
457
+ agent_id: subAgentId,
458
+ provider: selectedProvider,
459
+ project_id: currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null,
460
+ public_url: window.location.origin,
461
+ }),
462
+ });
463
+ const data = await res.json();
464
+ if (!res.ok) {
465
+ setError(data.error || "Failed to create subscription");
466
+ } else {
467
+ setShowAddSub(false);
468
+ setSubTriggerId("");
469
+ setSubAgentId("");
470
+ fetchSubscriptions();
471
+ }
472
+ } catch (e: any) {
473
+ setError(e.message || "Failed to create subscription");
474
+ }
475
+ setAddingSub(false);
476
+ };
477
+
478
+ // Toggle subscription
479
+ const toggleSubscription = async (sub: Subscription) => {
480
+ const action = sub.enabled ? "disable" : "enable";
481
+ try {
482
+ const res = await authFetch(`/api/subscriptions/${sub.id}/${action}`, {
483
+ method: "POST",
484
+ });
485
+ if (res.ok) fetchSubscriptions();
486
+ } catch (e) {
487
+ setError(`Failed to ${action} subscription`);
488
+ }
489
+ };
490
+
491
+ // Delete subscription
492
+ const deleteSubscription = async (id: string) => {
493
+ try {
494
+ const res = await authFetch(`/api/subscriptions/${id}`, {
495
+ method: "DELETE",
496
+ });
497
+ if (res.ok) fetchSubscriptions();
498
+ } catch (e) {
499
+ setError("Failed to delete subscription");
500
+ }
501
+ };
502
+
503
+ // Filter trigger types by search
504
+ const filteredTypes = triggerTypes.filter(t => {
505
+ if (!typeSearch) return true;
506
+ const s = typeSearch.toLowerCase();
507
+ return t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) || t.description.toLowerCase().includes(s);
508
+ });
509
+
510
+ // Auto-match connected account from toolkit slug
511
+ const matchAccount = (accounts: ConnectedAccount[], toolkitSlug: string): ConnectedAccount | null => {
512
+ if (!toolkitSlug || accounts.length === 0) return null;
513
+ const slug = toolkitSlug.toLowerCase();
514
+ return accounts.find(a =>
515
+ a.appId?.toLowerCase() === slug ||
516
+ a.appName?.toLowerCase() === slug ||
517
+ a.appId?.toLowerCase().includes(slug) ||
518
+ a.appName?.toLowerCase().includes(slug)
519
+ ) || null;
520
+ };
521
+
522
+ // Derived: auto-matched account for Add Subscription modal
523
+ const dojoSelectedTriggerType = dojoTriggerTypes.find(t => t.slug === dojoSelectedType);
524
+ const dojoMatchedAccount = dojoSelectedTriggerType ? matchAccount(dojoAccounts, dojoSelectedTriggerType.toolkit_slug) : null;
525
+
526
+ // Derived: auto-matched account for Browse Subscribe modal
527
+ const browseMatchedAccount = selectedType && isAgentDojo ? matchAccount(connectedAccounts, selectedType.toolkit_slug) : null;
528
+
529
+ // Agent map for quick lookups
530
+ const agentMap = new Map(agents.map(a => [a.id, a]));
531
+
532
+ return (
533
+ <div className="space-y-6">
534
+ {/* Error */}
535
+ {error && (
536
+ <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">
537
+ <span>{error}</span>
538
+ <button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">x</button>
539
+ </div>
540
+ )}
541
+
542
+ {/* Provider Selector */}
543
+ {providers.length > 1 && (
544
+ <div className="flex items-center gap-2">
545
+ <span className="text-xs text-[#666]">Provider:</span>
546
+ <div className="flex gap-1 bg-[#111] border border-[#1a1a1a] rounded-lg p-0.5">
547
+ {providers.map(p => (
548
+ <button
549
+ key={p.id}
550
+ onClick={() => {
551
+ setSelectedProvider(p.id);
552
+ setTriggerTypes([]);
553
+ setToolkitFilter("");
554
+ setTypeSearch("");
555
+ }}
556
+ className={`px-3 py-1 rounded text-xs font-medium transition ${
557
+ selectedProvider === p.id
558
+ ? "bg-[#1a1a1a] text-white"
559
+ : "text-[#666] hover:text-[#888]"
560
+ }`}
561
+ >
562
+ {p.name}
563
+ {!p.connected && (
564
+ <span className="ml-1 text-[10px] text-yellow-500" title="API key not configured">!</span>
565
+ )}
566
+ </button>
567
+ ))}
568
+ </div>
569
+ </div>
570
+ )}
571
+
572
+ {/* Subscriptions (trigger → agent routing) — hide entirely for AgentDojo (handled in Active Subscriptions) */}
573
+ {!isAgentDojo && (
574
+ <section>
575
+ <div className="flex items-center justify-between mb-3">
576
+ <h3 className="text-sm font-medium text-[#888]">
577
+ Subscriptions ({subscriptions.length})
578
+ </h3>
579
+ <button
580
+ onClick={() => setShowAddSub(true)}
581
+ className="text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-3 py-1.5 rounded transition"
582
+ >
583
+ + Add Subscription
584
+ </button>
585
+ </div>
586
+
587
+ {subscriptions.length === 0 ? (
588
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center text-[#666] text-sm">
589
+ No subscriptions yet. Add one to route trigger events to an agent.
590
+ </div>
591
+ ) : (
592
+ <div className="space-y-2">
593
+ {subscriptions.map(sub => {
594
+ const agent = agentMap.get(sub.agent_id);
595
+ return (
596
+ <div key={sub.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 flex items-center gap-3">
597
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${sub.enabled ? "bg-green-400" : "bg-[#666]"}`} />
598
+ <div className="flex-1 min-w-0">
599
+ <div className="text-sm font-medium truncate">
600
+ {sub.trigger_slug.replace(/_/g, " ")}
601
+ <span className="text-[#555] mx-1.5">&rarr;</span>
602
+ <span className="text-[#f97316]">{agent?.name || "Unknown Agent"}</span>
603
+ </div>
604
+ <div className="text-xs text-[#666]">
605
+ {sub.trigger_instance_id
606
+ ? `Instance: ${sub.trigger_instance_id.slice(0, 12)}...`
607
+ : "All instances"
608
+ }
609
+ </div>
610
+ </div>
611
+ <div className="flex items-center gap-2 flex-shrink-0">
612
+ <button
613
+ onClick={() => toggleSubscription(sub)}
614
+ className={`text-xs px-3 py-1 rounded transition ${
615
+ sub.enabled
616
+ ? "bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20"
617
+ : "bg-green-500/10 text-green-400 hover:bg-green-500/20"
618
+ }`}
619
+ >
620
+ {sub.enabled ? "Disable" : "Enable"}
621
+ </button>
622
+ <button
623
+ onClick={() => deleteSubscription(sub.id)}
624
+ className="text-xs text-[#666] hover:text-red-400 transition px-2"
625
+ >
626
+ Delete
627
+ </button>
628
+ </div>
629
+ </div>
630
+ );
631
+ })}
632
+ </div>
633
+ )}
634
+ </section>
635
+ )}
636
+
637
+ {/* Trigger Instances — only show for providers that have them (not AgentDojo) */}
638
+ {!isAgentDojo && (
639
+ <section>
640
+ <h3 className="text-sm font-medium text-[#888] mb-3">
641
+ Trigger Instances ({triggers.length})
642
+ </h3>
643
+ {triggersLoading ? (
644
+ <div className="text-center py-6 text-[#666] text-sm">Loading triggers...</div>
645
+ ) : triggers.length === 0 ? (
646
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center text-[#666] text-sm">
647
+ No trigger instances. Browse trigger types below to create one.
648
+ </div>
649
+ ) : (
650
+ <div className="space-y-2">
651
+ {triggers.map(trigger => (
652
+ <div key={trigger.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 flex items-center gap-3">
653
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${trigger.status === "active" ? "bg-green-400" : "bg-[#666]"}`} />
654
+ <div className="flex-1 min-w-0">
655
+ <div className="text-sm font-medium truncate">
656
+ {trigger.trigger_slug.replace(/_/g, " ")}
657
+ </div>
658
+ <div className="text-xs text-[#666]">
659
+ ID: {trigger.id.slice(0, 12)}... | Created: {new Date(trigger.created_at).toLocaleDateString()}
660
+ </div>
661
+ </div>
662
+ <div className="flex items-center gap-2 flex-shrink-0">
663
+ <button
664
+ onClick={() => toggleTrigger(trigger.id, trigger.status)}
665
+ className={`text-xs px-3 py-1 rounded transition ${
666
+ trigger.status === "active"
667
+ ? "bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20"
668
+ : "bg-green-500/10 text-green-400 hover:bg-green-500/20"
669
+ }`}
670
+ >
671
+ {trigger.status === "active" ? "Disable" : "Enable"}
672
+ </button>
673
+ <button
674
+ onClick={() => deleteTrigger(trigger.id)}
675
+ className="text-xs text-[#666] hover:text-red-400 transition px-2"
676
+ >
677
+ Delete
678
+ </button>
679
+ </div>
680
+ </div>
681
+ ))}
682
+ </div>
683
+ )}
684
+ </section>
685
+ )}
686
+
687
+ {/* AgentDojo Active Subscriptions — shows remote subscriptions directly */}
688
+ {isAgentDojo && (
689
+ <section>
690
+ <div className="flex items-center justify-between mb-3">
691
+ <h3 className="text-sm font-medium text-[#888]">
692
+ Active Subscriptions ({triggers.length})
693
+ </h3>
694
+ <button
695
+ onClick={openAddDojoSub}
696
+ className="text-xs bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-3 py-1.5 rounded transition"
697
+ >
698
+ + Add Subscription
699
+ </button>
700
+ </div>
701
+ {triggersLoading ? (
702
+ <div className="text-center py-6 text-[#666] text-sm">Loading subscriptions...</div>
703
+ ) : triggers.length === 0 ? (
704
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center text-[#666] text-sm">
705
+ No active subscriptions. Browse trigger types below to create one.
706
+ </div>
707
+ ) : (
708
+ <div className="space-y-2">
709
+ {triggers.map(trigger => {
710
+ const localSub = subscriptions.find(s => s.trigger_instance_id === trigger.id);
711
+ const agent = localSub ? agentMap.get(localSub.agent_id) : null;
712
+ return (
713
+ <div key={trigger.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 flex items-center gap-3">
714
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${trigger.status === "active" ? "bg-green-400" : "bg-[#666]"}`} />
715
+ <div className="flex-1 min-w-0">
716
+ <div className="text-sm font-medium truncate">
717
+ {(trigger.config?.title as string) || trigger.trigger_slug.replace(/_/g, " ")}
718
+ {agent && (
719
+ <>
720
+ <span className="text-[#555] mx-1.5">&rarr;</span>
721
+ <span className="text-[#f97316]">{agent.name}</span>
722
+ </>
723
+ )}
724
+ </div>
725
+ <div className="text-xs text-[#666]">
726
+ {trigger.config?.server && <span>{String(trigger.config.server)} | </span>}
727
+ ID: {String(trigger.id).slice(0, 8)} | Created: {new Date(trigger.created_at).toLocaleDateString()}
728
+ </div>
729
+ </div>
730
+ <div className="flex items-center gap-2 flex-shrink-0">
731
+ <button
732
+ onClick={() => toggleTrigger(trigger.id, trigger.status)}
733
+ className={`text-xs px-3 py-1 rounded transition ${
734
+ trigger.status === "active"
735
+ ? "bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20"
736
+ : "bg-green-500/10 text-green-400 hover:bg-green-500/20"
737
+ }`}
738
+ >
739
+ {trigger.status === "active" ? "Disable" : "Enable"}
740
+ </button>
741
+ <button
742
+ onClick={() => deleteTrigger(trigger.id)}
743
+ className="text-xs text-[#666] hover:text-red-400 transition px-2"
744
+ >
745
+ Delete
746
+ </button>
747
+ </div>
748
+ </div>
749
+ );
750
+ })}
751
+ </div>
752
+ )}
753
+ </section>
754
+ )}
755
+
756
+ {/* Browse Trigger Types */}
757
+ <section>
758
+ <h3 className="text-sm font-medium text-[#888] mb-3">Browse Trigger Types</h3>
759
+ <div className="flex gap-2 mb-3">
760
+ <input
761
+ type="text"
762
+ value={toolkitFilter}
763
+ onChange={(e) => setToolkitFilter(e.target.value)}
764
+ placeholder="Toolkit filter (e.g. github, gmail, slack)"
765
+ className="flex-1 bg-[#111] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
766
+ />
767
+ <button
768
+ onClick={() => browseTriggerTypes(toolkitFilter || undefined)}
769
+ disabled={typesLoading}
770
+ className="text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-4 py-2 rounded transition disabled:opacity-50"
771
+ >
772
+ {typesLoading ? "Loading..." : "Browse"}
773
+ </button>
774
+ </div>
775
+
776
+ {triggerTypes.length > 0 && (
777
+ <>
778
+ <input
779
+ type="text"
780
+ value={typeSearch}
781
+ onChange={(e) => setTypeSearch(e.target.value)}
782
+ placeholder="Search trigger types..."
783
+ className="w-full bg-[#111] border border-[#333] rounded px-3 py-2 text-sm mb-3 focus:outline-none focus:border-[#f97316]"
784
+ />
785
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
786
+ {filteredTypes.slice(0, 30).map(tt => (
787
+ <div key={tt.slug} className="bg-[#111] border border-[#1a1a1a] hover:border-[#333] rounded-lg p-3 transition">
788
+ <div className="flex items-start gap-3">
789
+ {tt.logo ? (
790
+ <img src={tt.logo} alt={tt.toolkit_name} className="w-8 h-8 rounded object-contain flex-shrink-0" />
791
+ ) : (
792
+ <div className="w-8 h-8 rounded bg-[#1a1a1a] flex items-center justify-center text-xs flex-shrink-0">
793
+ {tt.toolkit_name?.[0]?.toUpperCase() || "?"}
794
+ </div>
795
+ )}
796
+ <div className="flex-1 min-w-0">
797
+ <div className="text-sm font-medium truncate">{tt.name}</div>
798
+ <div className="text-xs text-[#666]">{tt.toolkit_name}</div>
799
+ <div className="text-xs text-[#555] mt-1 line-clamp-2">{tt.description}</div>
800
+ </div>
801
+ </div>
802
+ <button
803
+ onClick={() => startCreate(tt)}
804
+ 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"
805
+ >
806
+ {isAgentDojo ? "Subscribe" : "Create Trigger"}
807
+ </button>
808
+ </div>
809
+ ))}
810
+ </div>
811
+ {filteredTypes.length > 30 && (
812
+ <p className="text-xs text-[#555] mt-3 text-center">
813
+ Showing first 30 of {filteredTypes.length} types. Use search to filter.
814
+ </p>
815
+ )}
816
+ </>
817
+ )}
818
+ </section>
819
+
820
+ {/* Create Trigger Modal */}
821
+ {showCreate && selectedType && (
822
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
823
+ <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-md mx-4">
824
+ <h3 className="font-medium mb-1">
825
+ {isAgentDojo ? "Create Subscription" : "Create Trigger"}
826
+ </h3>
827
+ <p className="text-xs text-[#666] mb-4">
828
+ {selectedType.name}
829
+ {selectedType.toolkit_name && <span className="text-[#555]"> ({selectedType.toolkit_name})</span>}
830
+ </p>
831
+
832
+ <div className="space-y-4">
833
+ {/* Connected Account — only for Composio */}
834
+ {!isAgentDojo && (
835
+ <div>
836
+ <label className="block text-xs text-[#888] mb-1.5">Connected Account</label>
837
+ {connectedAccounts.length === 0 ? (
838
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
839
+ No connected accounts available. Connect an app first in the Integrations tab.
840
+ </div>
841
+ ) : (
842
+ <Select
843
+ value={selectedAccountId}
844
+ onChange={setSelectedAccountId}
845
+ placeholder="Select account..."
846
+ options={connectedAccounts.map(acc => ({
847
+ value: acc.id,
848
+ label: `${acc.appName} (${acc.id.slice(0, 8)}...)`,
849
+ }))}
850
+ />
851
+ )}
852
+ </div>
853
+ )}
854
+
855
+ {/* Agent selection — for AgentDojo direct subscription */}
856
+ {isAgentDojo && (
857
+ <div>
858
+ <label className="block text-xs text-[#888] mb-1.5">Route to Agent</label>
859
+ {agents.length === 0 ? (
860
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
861
+ No agents available. Create an agent first.
862
+ </div>
863
+ ) : (
864
+ <Select
865
+ value={createAgentId}
866
+ onChange={setCreateAgentId}
867
+ placeholder="Select agent..."
868
+ options={agents.map(agent => ({
869
+ value: agent.id,
870
+ label: `${agent.name} (${agent.status})`,
871
+ }))}
872
+ />
873
+ )}
874
+
875
+ {/* Connected account — auto-matched from toolkit */}
876
+ <div className="mt-3">
877
+ <label className="block text-xs text-[#888] mb-1.5">Connected Account</label>
878
+ {browseMatchedAccount ? (
879
+ <div className="text-xs text-green-400 bg-green-500/10 border border-green-500/20 rounded p-3">
880
+ Connected: {browseMatchedAccount.appName}
881
+ </div>
882
+ ) : (
883
+ <div className="text-xs text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded p-3">
884
+ No connected account for {selectedType?.toolkit_name || "this app"}. Connect it first in the Integrations tab.
885
+ </div>
886
+ )}
887
+ </div>
888
+
889
+ {/* Dynamic config fields from config_schema */}
890
+ {selectedType.config_schema && Object.keys((selectedType.config_schema as any).properties || {}).length > 0 && (
891
+ <div className="mt-3">
892
+ <label className="block text-xs text-[#888] mb-1.5">Configuration</label>
893
+ <div className="space-y-2">
894
+ {Object.entries((selectedType.config_schema as any).properties || {}).map(([key, schema]: [string, any]) => {
895
+ const required = ((selectedType.config_schema as any).required || []).includes(key);
896
+ return (
897
+ <div key={key}>
898
+ <label className="block text-[11px] text-[#888] mb-1">
899
+ {schema.title || key}
900
+ {required && <span className="text-red-400 ml-0.5">*</span>}
901
+ </label>
902
+ <input
903
+ type="text"
904
+ value={browseConfig[key] || ""}
905
+ onChange={(e) => setBrowseConfig(prev => ({ ...prev, [key]: e.target.value }))}
906
+ placeholder={schema.description || `Enter ${schema.title || key}...`}
907
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
908
+ />
909
+ </div>
910
+ );
911
+ })}
912
+ </div>
913
+ </div>
914
+ )}
915
+ </div>
916
+ )}
917
+ </div>
918
+
919
+ <div className="flex gap-2 mt-4">
920
+ <button
921
+ onClick={() => { setShowCreate(false); setSelectedType(null); }}
922
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
923
+ >
924
+ Cancel
925
+ </button>
926
+ <button
927
+ onClick={handleCreate}
928
+ disabled={isAgentDojo ? (
929
+ !createAgentId || !browseMatchedAccount || creating ||
930
+ (selectedType?.config_schema && ((selectedType.config_schema as any).required || []).some((key: string) => !browseConfig[key]?.trim()))
931
+ ) : (!selectedAccountId || creating)}
932
+ className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
933
+ >
934
+ {creating ? "Creating..." : isAgentDojo ? "Subscribe" : "Create"}
935
+ </button>
936
+ </div>
937
+ </div>
938
+ </div>
939
+ )}
940
+
941
+ {/* Add Subscription Modal */}
942
+ {showAddSub && (
943
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
944
+ <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-md mx-4">
945
+ <h3 className="font-medium mb-1">Route Trigger to Agent</h3>
946
+ <p className="text-xs text-[#666] mb-4">
947
+ {triggers.length === 0
948
+ ? "No trigger instances yet. Create one first from the Browse section below."
949
+ : "Select a trigger instance and the agent that should handle its events."
950
+ }
951
+ </p>
952
+
953
+ {triggers.length > 0 ? (
954
+ <>
955
+ <div className="space-y-4">
956
+ <div>
957
+ <label className="block text-xs text-[#888] mb-1.5">Trigger Instance</label>
958
+ <Select
959
+ value={subTriggerId}
960
+ onChange={setSubTriggerId}
961
+ placeholder="Select trigger..."
962
+ options={triggers.map(t => ({
963
+ value: t.id,
964
+ label: `${t.trigger_slug.replace(/_/g, " ")}`,
965
+ }))}
966
+ />
967
+ {subTriggerId && (
968
+ <div className="text-xs text-[#555] mt-1 font-mono">
969
+ ID: {subTriggerId.slice(0, 16)}...
970
+ </div>
971
+ )}
972
+ </div>
973
+
974
+ <div>
975
+ <label className="block text-xs text-[#888] mb-1.5">Target Agent</label>
976
+ <Select
977
+ value={subAgentId}
978
+ onChange={setSubAgentId}
979
+ placeholder="Select agent..."
980
+ options={agents.map(agent => ({
981
+ value: agent.id,
982
+ label: `${agent.name} (${agent.status})`,
983
+ }))}
984
+ />
985
+ </div>
986
+ </div>
987
+
988
+ <div className="flex gap-2 mt-5">
989
+ <button
990
+ onClick={() => { setShowAddSub(false); setSubTriggerId(""); setSubAgentId(""); }}
991
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
992
+ >
993
+ Cancel
994
+ </button>
995
+ <button
996
+ onClick={handleAddSubscription}
997
+ disabled={!subTriggerId || !subAgentId || addingSub}
998
+ className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
999
+ >
1000
+ {addingSub ? "Adding..." : "Add"}
1001
+ </button>
1002
+ </div>
1003
+ </>
1004
+ ) : (
1005
+ <div className="flex gap-2 mt-4">
1006
+ <button
1007
+ onClick={() => setShowAddSub(false)}
1008
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
1009
+ >
1010
+ Close
1011
+ </button>
1012
+ </div>
1013
+ )}
1014
+ </div>
1015
+ </div>
1016
+ )}
1017
+
1018
+ {/* AgentDojo Add Subscription Modal */}
1019
+ {showAddDojo && (
1020
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
1021
+ <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-lg mx-4">
1022
+ <h3 className="font-medium mb-1">Add Subscription</h3>
1023
+ <p className="text-xs text-[#666] mb-4">
1024
+ Select a trigger from your connected apps and route it to an agent.
1025
+ </p>
1026
+
1027
+ <div className="space-y-4">
1028
+ {/* Trigger type selection */}
1029
+ <div>
1030
+ <label className="block text-xs text-[#888] mb-1.5">Trigger</label>
1031
+ {dojoTypesLoading ? (
1032
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">Loading triggers...</div>
1033
+ ) : dojoTriggerTypes.length === 0 ? (
1034
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
1035
+ No triggers available. Connect an app first in the Integrations tab.
1036
+ </div>
1037
+ ) : (
1038
+ <>
1039
+ <input
1040
+ type="text"
1041
+ value={dojoTypeSearch}
1042
+ onChange={(e) => setDojoTypeSearch(e.target.value)}
1043
+ placeholder="Search triggers..."
1044
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm mb-2 focus:outline-none focus:border-[#f97316]"
1045
+ />
1046
+ <div className="max-h-48 overflow-y-auto border border-[#1a1a1a] rounded-lg">
1047
+ {dojoTriggerTypes
1048
+ .filter(t => {
1049
+ if (!dojoTypeSearch) return true;
1050
+ const s = dojoTypeSearch.toLowerCase();
1051
+ return t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) || t.toolkit_name.toLowerCase().includes(s);
1052
+ })
1053
+ .slice(0, 50)
1054
+ .map(t => (
1055
+ <button
1056
+ key={t.slug}
1057
+ onClick={() => { setDojoSelectedType(t.slug); setDojoConfig({}); }}
1058
+ className={`w-full text-left px-3 py-2 text-sm flex items-center gap-2 transition border-b border-[#1a1a1a] last:border-0 ${
1059
+ dojoSelectedType === t.slug
1060
+ ? "bg-[#f97316]/10 text-[#f97316]"
1061
+ : "hover:bg-[#1a1a1a] text-[#ccc]"
1062
+ }`}
1063
+ >
1064
+ {t.logo ? (
1065
+ <img src={t.logo} alt="" className="w-5 h-5 rounded object-contain flex-shrink-0" />
1066
+ ) : (
1067
+ <div className="w-5 h-5 rounded bg-[#1a1a1a] flex items-center justify-center text-[10px] flex-shrink-0">
1068
+ {t.toolkit_name?.[0]?.toUpperCase() || "?"}
1069
+ </div>
1070
+ )}
1071
+ <div className="flex-1 min-w-0">
1072
+ <div className="truncate">{t.name}</div>
1073
+ <div className="text-[10px] text-[#666] truncate">{t.toolkit_name}</div>
1074
+ </div>
1075
+ </button>
1076
+ ))}
1077
+ </div>
1078
+ </>
1079
+ )}
1080
+ </div>
1081
+
1082
+ {/* Connected account — auto-matched */}
1083
+ {dojoSelectedType && (
1084
+ <div>
1085
+ <label className="block text-xs text-[#888] mb-1.5">Connected Account</label>
1086
+ {dojoMatchedAccount ? (
1087
+ <div className="text-xs text-green-400 bg-green-500/10 border border-green-500/20 rounded p-3">
1088
+ Connected: {dojoMatchedAccount.appName}
1089
+ </div>
1090
+ ) : (
1091
+ <div className="text-xs text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded p-3">
1092
+ No connected account for {dojoSelectedTriggerType?.toolkit_name || "this app"}. Connect it first in the Integrations tab.
1093
+ </div>
1094
+ )}
1095
+ </div>
1096
+ )}
1097
+
1098
+ {/* Dynamic config fields from config_schema */}
1099
+ {dojoSelectedTriggerType && dojoSelectedTriggerType.config_schema && Object.keys(dojoSelectedTriggerType.config_schema.properties || {}).length > 0 && (
1100
+ <div>
1101
+ <label className="block text-xs text-[#888] mb-1.5">Configuration</label>
1102
+ <div className="space-y-2">
1103
+ {Object.entries((dojoSelectedTriggerType.config_schema as any).properties || {}).map(([key, schema]: [string, any]) => {
1104
+ const required = ((dojoSelectedTriggerType.config_schema as any).required || []).includes(key);
1105
+ return (
1106
+ <div key={key}>
1107
+ <label className="block text-[11px] text-[#888] mb-1">
1108
+ {schema.title || key}
1109
+ {required && <span className="text-red-400 ml-0.5">*</span>}
1110
+ </label>
1111
+ <input
1112
+ type="text"
1113
+ value={dojoConfig[key] || ""}
1114
+ onChange={(e) => setDojoConfig(prev => ({ ...prev, [key]: e.target.value }))}
1115
+ placeholder={schema.description || `Enter ${schema.title || key}...`}
1116
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
1117
+ />
1118
+ </div>
1119
+ );
1120
+ })}
1121
+ </div>
1122
+ </div>
1123
+ )}
1124
+
1125
+ {/* Agent selection */}
1126
+ <div>
1127
+ <label className="block text-xs text-[#888] mb-1.5">Target Agent</label>
1128
+ {agents.length === 0 ? (
1129
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
1130
+ No agents available. Create an agent first.
1131
+ </div>
1132
+ ) : (
1133
+ <Select
1134
+ value={dojoAgentId}
1135
+ onChange={setDojoAgentId}
1136
+ placeholder="Select agent..."
1137
+ options={agents.map(agent => ({
1138
+ value: agent.id,
1139
+ label: `${agent.name} (${agent.status})`,
1140
+ }))}
1141
+ />
1142
+ )}
1143
+ </div>
1144
+ </div>
1145
+
1146
+ <div className="flex gap-2 mt-5">
1147
+ <button
1148
+ onClick={() => setShowAddDojo(false)}
1149
+ className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
1150
+ >
1151
+ Cancel
1152
+ </button>
1153
+ <button
1154
+ onClick={handleAddDojoSub}
1155
+ disabled={!dojoSelectedType || !dojoAgentId || !dojoMatchedAccount || dojoCreating || (
1156
+ dojoSelectedTriggerType?.config_schema &&
1157
+ ((dojoSelectedTriggerType.config_schema as any).required || []).some((key: string) => !dojoConfig[key]?.trim())
1158
+ )}
1159
+ className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
1160
+ >
1161
+ {dojoCreating ? "Creating..." : "Subscribe"}
1162
+ </button>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ )}
1167
+ </div>
1168
+ );
1169
+ }