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
@@ -41,6 +41,13 @@ interface ConnectedAccount {
41
41
  status: string;
42
42
  }
43
43
 
44
+ interface IntegrationApp {
45
+ id: string;
46
+ name: string;
47
+ slug: string;
48
+ logo: string | null;
49
+ }
50
+
44
51
  interface Agent {
45
52
  id: string;
46
53
  name: string;
@@ -58,9 +65,9 @@ export function TriggersTab() {
58
65
  const { authFetch } = useAuth();
59
66
  const { currentProjectId } = useProjects();
60
67
 
61
- // Provider selection
68
+ // Provider selection — only show configured providers
62
69
  const [providers, setProviders] = useState<TriggerProviderInfo[]>([]);
63
- const [selectedProvider, setSelectedProvider] = useState("composio");
70
+ const [selectedProvider, setSelectedProvider] = useState("");
64
71
 
65
72
  // Trigger instances (from selected provider)
66
73
  const [triggers, setTriggers] = useState<TriggerInstance[]>([]);
@@ -83,17 +90,24 @@ export function TriggersTab() {
83
90
  const [creating, setCreating] = useState(false);
84
91
  const [createAgentId, setCreateAgentId] = useState(""); // For AgentDojo direct subscription flow
85
92
  const [browseConfig, setBrowseConfig] = useState<Record<string, string>>({});
93
+ const [browseSelectedAccountId, setBrowseSelectedAccountId] = useState("");
86
94
 
87
- // AgentDojo add subscription modal — uses composio as data source (same as Integrations tab)
95
+ // AgentDojo add subscription modal
88
96
  const [showAddDojo, setShowAddDojo] = useState(false);
89
97
  const [dojoTriggerTypes, setDojoTriggerTypes] = useState<TriggerType[]>([]);
90
98
  const [dojoTypesLoading, setDojoTypesLoading] = useState(false);
91
99
  const [dojoAccounts, setDojoAccounts] = useState<ConnectedAccount[]>([]);
100
+ const [dojoApps, setDojoApps] = useState<IntegrationApp[]>([]);
101
+ const [dojoSelectedToolkit, setDojoSelectedToolkit] = useState("");
92
102
  const [dojoSelectedType, setDojoSelectedType] = useState<string>("");
93
103
  const [dojoAgentId, setDojoAgentId] = useState("");
94
104
  const [dojoCreating, setDojoCreating] = useState(false);
95
- const [dojoTypeSearch, setDojoTypeSearch] = useState("");
96
105
  const [dojoConfig, setDojoConfig] = useState<Record<string, string>>({});
106
+ const [dojoSelectedAccountId, setDojoSelectedAccountId] = useState("");
107
+ const [dojoAppDropdownOpen, setDojoAppDropdownOpen] = useState(false);
108
+ const [dojoAppSearch, setDojoAppSearch] = useState("");
109
+ const [dojoTriggerDropdownOpen, setDojoTriggerDropdownOpen] = useState(false);
110
+ const [dojoTriggerSearch, setDojoTriggerSearch] = useState("");
97
111
 
98
112
  // Add subscription
99
113
  const [showAddSub, setShowAddSub] = useState(false);
@@ -108,13 +122,21 @@ export function TriggersTab() {
108
122
 
109
123
  const projectParam = currentProjectId && currentProjectId !== "unassigned" ? `?project_id=${currentProjectId}` : "";
110
124
 
111
- // Fetch available providers
125
+ // Fetch available providers — only show ones with API keys configured
112
126
  const fetchProviders = useCallback(async () => {
113
127
  try {
114
128
  const res = await authFetch(`/api/triggers/providers${projectParam}`);
115
129
  if (res.ok) {
116
130
  const data = await res.json();
117
- setProviders(data.providers || []);
131
+ const connected = (data.providers || []).filter((p: TriggerProviderInfo) => p.connected);
132
+ setProviders(connected);
133
+ // Auto-select first connected provider if none selected
134
+ if (connected.length > 0) {
135
+ setSelectedProvider(prev => {
136
+ if (!prev || !connected.find((p: TriggerProviderInfo) => p.id === prev)) return connected[0].id;
137
+ return prev;
138
+ });
139
+ }
118
140
  }
119
141
  } catch (e) {
120
142
  console.error("Failed to fetch providers:", e);
@@ -154,10 +176,10 @@ export function TriggersTab() {
154
176
  }
155
177
  }, [authFetch, projectParam]);
156
178
 
157
- // Fetch agents
179
+ // Fetch agents (project-scoped)
158
180
  const fetchAgents = useCallback(async () => {
159
181
  try {
160
- const res = await authFetch(`/api/agents`);
182
+ const res = await authFetch(`/api/agents${projectParam}`);
161
183
  if (res.ok) {
162
184
  const data = await res.json();
163
185
  setAgents(data.agents || []);
@@ -165,7 +187,7 @@ export function TriggersTab() {
165
187
  } catch (e) {
166
188
  // Ignore
167
189
  }
168
- }, [authFetch]);
190
+ }, [authFetch, projectParam]);
169
191
 
170
192
  useEffect(() => {
171
193
  fetchProviders();
@@ -214,6 +236,7 @@ export function TriggersTab() {
214
236
  setSelectedAccountId("");
215
237
  setCreateAgentId("");
216
238
  setBrowseConfig({});
239
+ setBrowseSelectedAccountId("");
217
240
  setShowCreate(true);
218
241
  fetchConnectedAccounts();
219
242
  };
@@ -224,65 +247,58 @@ export function TriggersTab() {
224
247
  const openAddDojoSub = async () => {
225
248
  setShowAddDojo(true);
226
249
  setDojoSelectedType("");
250
+ setDojoSelectedToolkit("");
227
251
  setDojoAgentId("");
228
- setDojoTypeSearch("");
229
252
  setDojoConfig({});
253
+ setDojoSelectedAccountId("");
230
254
 
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
255
  const loadTypes = async () => {
237
- if (dojoTriggerTypes.length > 0) {
238
- console.log("[openAddDojoSub] Already have", dojoTriggerTypes.length, "trigger types, skipping fetch");
239
- return;
240
- }
256
+ if (dojoTriggerTypes.length > 0) return;
241
257
  setDojoTypesLoading(true);
242
258
  try {
243
259
  let url = `/api/triggers/types?provider=agentdojo`;
244
260
  if (currentProjectId && currentProjectId !== "unassigned") url += `&project_id=${currentProjectId}`;
245
- console.log("[openAddDojoSub] Fetching trigger types:", url);
246
261
  const res = await authFetch(url);
247
262
  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
263
  setDojoTriggerTypes(data.types || []);
253
264
  } catch (e) {
254
- console.error("[openAddDojoSub] Failed to load trigger types:", e);
265
+ console.error("Failed to load trigger types:", e);
255
266
  }
256
267
  setDojoTypesLoading(false);
257
268
  };
258
269
 
259
- // Fetch connected accounts from agentdojo (same as Integrations tab)
260
270
  const loadAccounts = async () => {
261
271
  try {
262
272
  const url = `/api/integrations/agentdojo/connected${projectParam}`;
263
- console.log("[openAddDojoSub] Fetching connected accounts:", url);
264
273
  const res = await authFetch(url);
265
274
  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
275
  const active = (data.accounts || []).filter((a: ConnectedAccount) => a.status === "active");
271
- console.log("[openAddDojoSub] Active accounts:", active.length);
272
276
  setDojoAccounts(active);
273
277
  } catch (e) {
274
- console.error("[openAddDojoSub] Failed to load connected accounts:", e);
278
+ console.error("Failed to load connected accounts:", e);
275
279
  }
276
280
  };
277
281
 
278
- await Promise.all([loadTypes(), loadAccounts()]);
282
+ const loadApps = async () => {
283
+ if (dojoApps.length > 0) return;
284
+ try {
285
+ const url = `/api/integrations/agentdojo/apps${projectParam}`;
286
+ const res = await authFetch(url);
287
+ const data = await res.json();
288
+ setDojoApps((data.apps || []).map((a: any) => ({ id: a.id, name: a.name, slug: a.slug, logo: a.logo })));
289
+ } catch (e) {
290
+ console.error("Failed to load apps:", e);
291
+ }
292
+ };
293
+
294
+ await Promise.all([loadTypes(), loadAccounts(), loadApps()]);
279
295
  };
280
296
 
281
297
  // Create AgentDojo subscription from the add-subscription modal
282
298
  const handleAddDojoSub = async () => {
283
299
  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);
300
+ // Use derived dojoMatchedAccount (respects user dropdown selection + auto-match fallback)
301
+ const matched = dojoMatchedAccount;
286
302
  if (!tt || !dojoAgentId || !matched) return;
287
303
 
288
304
  setDojoCreating(true);
@@ -298,9 +314,8 @@ export function TriggersTab() {
298
314
  title: `${tt.name} → ${agent?.name || "Agent"}`,
299
315
  server: tt.toolkit_slug,
300
316
  agent_id: dojoAgentId,
301
- ...dojoConfig, // Merge dynamic config fields (e.g. owner, repo for GitHub)
317
+ ...dojoConfig,
302
318
  };
303
- console.log("[handleAddDojoSub] config:", JSON.stringify(configPayload));
304
319
  const res = await authFetch(url, {
305
320
  method: "POST",
306
321
  headers: { "Content-Type": "application/json" },
@@ -507,28 +522,85 @@ export function TriggersTab() {
507
522
  return t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) || t.description.toLowerCase().includes(s);
508
523
  });
509
524
 
510
- // Auto-match connected account from toolkit slug
525
+ // Best-effort match connected account from toolkit slug
526
+ // A single credential can serve multiple toolkits (e.g. "OmniKit Platform" for "OmniKit Messaging")
511
527
  const matchAccount = (accounts: ConnectedAccount[], toolkitSlug: string): ConnectedAccount | null => {
512
528
  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 ||
529
+ const slug = toolkitSlug.toLowerCase().replace(/[-_]/g, " ");
530
+ // Exact match
531
+ const exact = accounts.find(a =>
532
+ a.appId?.toLowerCase() === toolkitSlug.toLowerCase() ||
533
+ a.appName?.toLowerCase() === toolkitSlug.toLowerCase()
534
+ );
535
+ if (exact) return exact;
536
+ // Contains match
537
+ const contains = accounts.find(a =>
517
538
  a.appId?.toLowerCase().includes(slug) ||
518
- a.appName?.toLowerCase().includes(slug)
519
- ) || null;
539
+ a.appName?.toLowerCase().replace(/[-_]/g, " ").includes(slug) ||
540
+ slug.includes(a.appId?.toLowerCase() || "") ||
541
+ slug.includes(a.appName?.toLowerCase().replace(/[-_]/g, " ") || "")
542
+ );
543
+ if (contains) return contains;
544
+ // Prefix match — first word overlap (e.g. "omnikit" matches "omnikit platform" and "omnikit messaging")
545
+ const slugWords = slug.split(/\s+/);
546
+ return accounts.find(a => {
547
+ const nameWords = (a.appName || "").toLowerCase().replace(/[-_]/g, " ").split(/\s+/);
548
+ return slugWords[0] && nameWords[0] && slugWords[0] === nameWords[0];
549
+ }) || null;
520
550
  };
521
551
 
522
- // Derived: auto-matched account for Add Subscription modal
552
+ // Derived: auto-matched or user-selected account for Add Subscription modal
523
553
  const dojoSelectedTriggerType = dojoTriggerTypes.find(t => t.slug === dojoSelectedType);
524
- const dojoMatchedAccount = dojoSelectedTriggerType ? matchAccount(dojoAccounts, dojoSelectedTriggerType.toolkit_slug) : null;
554
+ const dojoAutoMatch = dojoSelectedTriggerType ? matchAccount(dojoAccounts, dojoSelectedTriggerType.toolkit_slug) : null;
555
+ const dojoMatchedAccount = dojoSelectedAccountId
556
+ ? dojoAccounts.find(a => a.id === dojoSelectedAccountId) || dojoAutoMatch
557
+ : dojoAutoMatch;
558
+
559
+ // Derived: auto-matched or user-selected account for Browse Subscribe modal
560
+ const browseAutoMatch = selectedType && isAgentDojo ? matchAccount(connectedAccounts, selectedType.toolkit_slug) : null;
561
+ const browseMatchedAccount = browseSelectedAccountId
562
+ ? connectedAccounts.find(a => a.id === browseSelectedAccountId) || browseAutoMatch
563
+ : browseAutoMatch;
564
+
565
+ // Derived: group trigger types by toolkit, enriched with logos from apps list
566
+ const dojoToolkits = React.useMemo(() => {
567
+ const appLogos = new Map<string, string>();
568
+ for (const app of dojoApps) {
569
+ if (app.logo) appLogos.set(app.slug, app.logo);
570
+ }
571
+ const map = new Map<string, { slug: string; name: string; logo: string | null; count: number }>();
572
+ for (const t of dojoTriggerTypes) {
573
+ const existing = map.get(t.toolkit_slug);
574
+ if (existing) {
575
+ existing.count++;
576
+ } else {
577
+ const logo = appLogos.get(t.toolkit_slug) || t.logo || null;
578
+ map.set(t.toolkit_slug, { slug: t.toolkit_slug, name: t.toolkit_name, logo, count: 1 });
579
+ }
580
+ }
581
+ return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name));
582
+ }, [dojoTriggerTypes, dojoApps]);
583
+
584
+ // Derived: triggers for the selected toolkit
585
+ const dojoToolkitTriggers = dojoSelectedToolkit
586
+ ? dojoTriggerTypes.filter(t => t.toolkit_slug === dojoSelectedToolkit)
587
+ : [];
525
588
 
526
- // Derived: auto-matched account for Browse Subscribe modal
527
- const browseMatchedAccount = selectedType && isAgentDojo ? matchAccount(connectedAccounts, selectedType.toolkit_slug) : null;
589
+ // Derived: selected toolkit info (for logo in trigger dropdown)
590
+ const dojoSelectedToolkitInfo = dojoToolkits.find(t => t.slug === dojoSelectedToolkit);
528
591
 
529
592
  // Agent map for quick lookups
530
593
  const agentMap = new Map(agents.map(a => [a.id, a]));
531
594
 
595
+ if (providers.length === 0 && !triggersLoading) {
596
+ return (
597
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-8 text-center">
598
+ <p className="text-[#666]">No trigger providers configured.</p>
599
+ <p className="text-sm text-[#555] mt-1">Add API keys for Composio or AgentDojo in Settings to enable triggers.</p>
600
+ </div>
601
+ );
602
+ }
603
+
532
604
  return (
533
605
  <div className="space-y-6">
534
606
  {/* Error */}
@@ -539,7 +611,7 @@ export function TriggersTab() {
539
611
  </div>
540
612
  )}
541
613
 
542
- {/* Provider Selector */}
614
+ {/* Provider Selector — only show if multiple configured */}
543
615
  {providers.length > 1 && (
544
616
  <div className="flex items-center gap-2">
545
617
  <span className="text-xs text-[#666]">Provider:</span>
@@ -560,9 +632,6 @@ export function TriggersTab() {
560
632
  }`}
561
633
  >
562
634
  {p.name}
563
- {!p.connected && (
564
- <span className="ml-1 text-[10px] text-yellow-500" title="API key not configured">!</span>
565
- )}
566
635
  </button>
567
636
  ))}
568
637
  </div>
@@ -1021,127 +1090,235 @@ export function TriggersTab() {
1021
1090
  <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-lg mx-4">
1022
1091
  <h3 className="font-medium mb-1">Add Subscription</h3>
1023
1092
  <p className="text-xs text-[#666] mb-4">
1024
- Select a trigger from your connected apps and route it to an agent.
1093
+ Select an app and trigger, then route it to an agent.
1025
1094
  </p>
1026
1095
 
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.
1096
+ {dojoTypesLoading ? (
1097
+ <div className="text-center py-8 text-[#666] text-sm">Loading...</div>
1098
+ ) : dojoTriggerTypes.length === 0 ? (
1099
+ <div className="text-center py-8 text-[#666] text-sm">
1100
+ No triggers available. Connect an app first in the Integrations tab.
1101
+ </div>
1102
+ ) : (
1103
+ <div className="space-y-4">
1104
+ {/* App selector custom dropdown with logos */}
1105
+ <div>
1106
+ <label className="block text-xs text-[#888] mb-1.5">App</label>
1107
+ <div className="relative">
1108
+ <button
1109
+ onClick={() => { setDojoAppDropdownOpen(!dojoAppDropdownOpen); setDojoTriggerDropdownOpen(false); setDojoAppSearch(""); }}
1110
+ className="w-full flex items-center gap-2 bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm text-left hover:border-[#555] transition"
1111
+ >
1112
+ {dojoSelectedToolkitInfo ? (
1113
+ <>
1114
+ {dojoSelectedToolkitInfo.logo ? (
1115
+ <img src={dojoSelectedToolkitInfo.logo} alt="" className="w-5 h-5 rounded object-contain flex-shrink-0" />
1116
+ ) : (
1117
+ <div className="w-5 h-5 rounded bg-[#1a1a1a] flex items-center justify-center text-[10px] flex-shrink-0">
1118
+ {dojoSelectedToolkitInfo.name?.[0]?.toUpperCase() || "?"}
1119
+ </div>
1120
+ )}
1121
+ <span className="flex-1 truncate">{dojoSelectedToolkitInfo.name}</span>
1122
+ <span className="text-[10px] text-[#666]">{dojoSelectedToolkitInfo.count} triggers</span>
1123
+ </>
1124
+ ) : (
1125
+ <span className="text-[#666] flex-1">Select app...</span>
1126
+ )}
1127
+ <span className="text-[#666] text-xs ml-1">&#9662;</span>
1128
+ </button>
1129
+ {dojoAppDropdownOpen && (
1130
+ <>
1131
+ <div className="fixed inset-0 z-10" onClick={() => setDojoAppDropdownOpen(false)} />
1132
+ <div className="absolute left-0 right-0 top-full mt-1 bg-[#0a0a0a] border border-[#333] rounded-lg shadow-xl z-20 max-h-64 flex flex-col">
1133
+ <div className="p-2 border-b border-[#1a1a1a] flex-shrink-0">
1134
+ <input
1135
+ type="text"
1136
+ value={dojoAppSearch}
1137
+ onChange={(e) => setDojoAppSearch(e.target.value)}
1138
+ placeholder="Search apps..."
1139
+ className="w-full bg-[#111] border border-[#333] rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[#f97316]"
1140
+ autoFocus
1141
+ />
1142
+ </div>
1143
+ <div className="overflow-y-auto flex-1">
1144
+ {dojoToolkits
1145
+ .filter(tk => {
1146
+ if (!dojoAppSearch) return true;
1147
+ const s = dojoAppSearch.toLowerCase();
1148
+ return tk.name.toLowerCase().includes(s) || tk.slug.toLowerCase().includes(s);
1149
+ })
1150
+ .map(tk => (
1151
+ <button
1152
+ key={tk.slug}
1153
+ onClick={() => {
1154
+ setDojoSelectedToolkit(tk.slug);
1155
+ setDojoSelectedType("");
1156
+ setDojoConfig({});
1157
+ setDojoAppDropdownOpen(false);
1158
+ }}
1159
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition hover:bg-[#1a1a1a] ${
1160
+ dojoSelectedToolkit === tk.slug ? "bg-[#1a1a1a] text-[#f97316]" : ""
1161
+ }`}
1162
+ >
1163
+ {tk.logo ? (
1164
+ <img src={tk.logo} alt="" className="w-5 h-5 rounded object-contain flex-shrink-0" />
1165
+ ) : (
1166
+ <div className="w-5 h-5 rounded bg-[#1a1a1a] flex items-center justify-center text-[10px] flex-shrink-0">
1167
+ {tk.name?.[0]?.toUpperCase() || "?"}
1168
+ </div>
1169
+ )}
1170
+ <span className="flex-1 truncate">{tk.name}</span>
1171
+ <span className="text-[10px] text-[#666]">{tk.count}</span>
1172
+ </button>
1173
+ ))}
1174
+ </div>
1175
+ </div>
1176
+ </>
1177
+ )}
1036
1178
  </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() || "?"}
1179
+ </div>
1180
+
1181
+ {/* Trigger selector — only shown when app is selected */}
1182
+ {dojoSelectedToolkit && (
1183
+ <div>
1184
+ <label className="block text-xs text-[#888] mb-1.5">Trigger</label>
1185
+ <div className="relative">
1186
+ <button
1187
+ onClick={() => { setDojoTriggerDropdownOpen(!dojoTriggerDropdownOpen); setDojoAppDropdownOpen(false); setDojoTriggerSearch(""); }}
1188
+ className="w-full flex items-center gap-2 bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm text-left hover:border-[#555] transition"
1189
+ >
1190
+ {dojoSelectedTriggerType ? (
1191
+ <>
1192
+ <span className="flex-1 truncate">{dojoSelectedTriggerType.name}</span>
1193
+ <span className={`text-[10px] px-1.5 py-0.5 rounded flex-shrink-0 ${
1194
+ dojoSelectedTriggerType.type === "webhook" ? "bg-blue-500/10 text-blue-400" : "bg-yellow-500/10 text-yellow-400"
1195
+ }`}>
1196
+ {dojoSelectedTriggerType.type}
1197
+ </span>
1198
+ </>
1199
+ ) : (
1200
+ <span className="text-[#666] flex-1">Select trigger...</span>
1201
+ )}
1202
+ <span className="text-[#666] text-xs ml-1">&#9662;</span>
1203
+ </button>
1204
+ {dojoTriggerDropdownOpen && (
1205
+ <>
1206
+ <div className="fixed inset-0 z-10" onClick={() => setDojoTriggerDropdownOpen(false)} />
1207
+ <div className="absolute left-0 right-0 top-full mt-1 bg-[#0a0a0a] border border-[#333] rounded-lg shadow-xl z-20 max-h-64 flex flex-col">
1208
+ {dojoToolkitTriggers.length > 3 && (
1209
+ <div className="p-2 border-b border-[#1a1a1a] flex-shrink-0">
1210
+ <input
1211
+ type="text"
1212
+ value={dojoTriggerSearch}
1213
+ onChange={(e) => setDojoTriggerSearch(e.target.value)}
1214
+ placeholder="Search triggers..."
1215
+ className="w-full bg-[#111] border border-[#333] rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[#f97316]"
1216
+ autoFocus
1217
+ />
1069
1218
  </div>
1070
1219
  )}
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>
1220
+ <div className="overflow-y-auto flex-1">
1221
+ {dojoToolkitTriggers
1222
+ .filter(t => {
1223
+ if (!dojoTriggerSearch) return true;
1224
+ const s = dojoTriggerSearch.toLowerCase();
1225
+ return t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) || t.description.toLowerCase().includes(s);
1226
+ })
1227
+ .map(t => (
1228
+ <button
1229
+ key={t.slug}
1230
+ onClick={() => {
1231
+ setDojoSelectedType(t.slug);
1232
+ setDojoConfig({});
1233
+ setDojoTriggerDropdownOpen(false);
1234
+ }}
1235
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition hover:bg-[#1a1a1a] ${
1236
+ dojoSelectedType === t.slug ? "bg-[#1a1a1a] text-[#f97316]" : ""
1237
+ }`}
1238
+ >
1239
+ <div className="flex-1 min-w-0">
1240
+ <div className="truncate">{t.name}</div>
1241
+ <div className="text-[10px] text-[#666] truncate">{t.description}</div>
1242
+ </div>
1243
+ <span className={`text-[10px] px-1.5 py-0.5 rounded flex-shrink-0 ${
1244
+ t.type === "webhook" ? "bg-blue-500/10 text-blue-400" : "bg-yellow-500/10 text-yellow-400"
1245
+ }`}>
1246
+ {t.type}
1247
+ </span>
1248
+ </button>
1249
+ ))}
1074
1250
  </div>
1075
- </button>
1076
- ))}
1251
+ </div>
1252
+ </>
1253
+ )}
1077
1254
  </div>
1078
- </>
1255
+ </div>
1256
+ )}
1257
+
1258
+ {/* Connected account — auto-matched */}
1259
+ {dojoSelectedType && (
1260
+ <div>
1261
+ <label className="block text-xs text-[#888] mb-1.5">Connected Account</label>
1262
+ {dojoMatchedAccount ? (
1263
+ <div className="text-xs text-green-400 bg-green-500/10 border border-green-500/20 rounded p-3">
1264
+ Connected: {dojoMatchedAccount.appName}
1265
+ </div>
1266
+ ) : (
1267
+ <div className="text-xs text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded p-3">
1268
+ No connected account for {dojoSelectedTriggerType?.toolkit_name || "this app"}. Connect it first in the Integrations tab.
1269
+ </div>
1270
+ )}
1271
+ </div>
1079
1272
  )}
1080
- </div>
1081
1273
 
1082
- {/* Connected account auto-matched */}
1083
- {dojoSelectedType && (
1274
+ {/* Dynamic config fields from config_schema */}
1275
+ {dojoSelectedTriggerType && dojoSelectedTriggerType.config_schema && Object.keys(dojoSelectedTriggerType.config_schema.properties || {}).length > 0 && (
1276
+ <div>
1277
+ <label className="block text-xs text-[#888] mb-1.5">Configuration</label>
1278
+ <div className="space-y-2">
1279
+ {Object.entries((dojoSelectedTriggerType.config_schema as any).properties || {}).map(([key, schema]: [string, any]) => {
1280
+ const required = ((dojoSelectedTriggerType.config_schema as any).required || []).includes(key);
1281
+ return (
1282
+ <div key={key}>
1283
+ <label className="block text-[11px] text-[#888] mb-1">
1284
+ {schema.title || key}
1285
+ {required && <span className="text-red-400 ml-0.5">*</span>}
1286
+ </label>
1287
+ <input
1288
+ type="text"
1289
+ value={dojoConfig[key] || ""}
1290
+ onChange={(e) => setDojoConfig(prev => ({ ...prev, [key]: e.target.value }))}
1291
+ placeholder={schema.description || `Enter ${schema.title || key}...`}
1292
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
1293
+ />
1294
+ </div>
1295
+ );
1296
+ })}
1297
+ </div>
1298
+ </div>
1299
+ )}
1300
+
1301
+ {/* Agent selection */}
1084
1302
  <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}
1303
+ <label className="block text-xs text-[#888] mb-1.5">Target Agent</label>
1304
+ {agents.length === 0 ? (
1305
+ <div className="text-xs text-[#666] bg-[#0a0a0a] rounded p-3">
1306
+ No agents available. Create an agent first.
1089
1307
  </div>
1090
1308
  ) : (
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>
1309
+ <Select
1310
+ value={dojoAgentId}
1311
+ onChange={setDojoAgentId}
1312
+ placeholder="Select agent..."
1313
+ options={agents.map(agent => ({
1314
+ value: agent.id,
1315
+ label: `${agent.name} (${agent.status})`,
1316
+ }))}
1317
+ />
1094
1318
  )}
1095
1319
  </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
1320
  </div>
1144
- </div>
1321
+ )}
1145
1322
 
1146
1323
  <div className="flex gap-2 mt-5">
1147
1324
  <button