apteva 0.4.31 → 0.4.41

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 (89) hide show
  1. package/dist/ActivityPage.7907h64p.js +3 -0
  2. package/dist/ApiDocsPage.k3jjenpq.js +4 -0
  3. package/dist/App.01nq20st.js +4 -0
  4. package/dist/App.1maqvamf.js +4 -0
  5. package/dist/App.2yjrh32f.js +4 -0
  6. package/dist/App.3qw8nben.js +20 -0
  7. package/dist/App.7fb3e7mp.js +4 -0
  8. package/dist/App.7sy3wq8c.js +4 -0
  9. package/dist/App.apjrmctz.js +57 -0
  10. package/dist/App.av6t2yhe.js +4 -0
  11. package/dist/App.jqj5a094.js +46 -0
  12. package/dist/App.mc7xf85h.js +4 -0
  13. package/dist/App.myxqcj9x.js +4 -0
  14. package/dist/App.nm91r1mp.js +13 -0
  15. package/dist/App.qcknavjz.js +221 -0
  16. package/dist/App.vc7vfhg4.js +4 -0
  17. package/dist/App.z4s9zkw5.js +4 -0
  18. package/dist/ConnectionsPage.z1pw5xe2.js +3 -0
  19. package/dist/McpPage.8vc97z0b.js +3 -0
  20. package/dist/SettingsPage.p61bz8kd.js +3 -0
  21. package/dist/SkillsPage.r9x43g3g.js +3 -0
  22. package/dist/TasksPage.1e0zkye4.js +3 -0
  23. package/dist/TelemetryPage.p9vbe4gf.js +3 -0
  24. package/dist/TestsPage.d4xy504e.js +3 -0
  25. package/dist/ThreadsPage.m016am3x.js +3 -0
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +8 -7
  29. package/src/crypto.ts +4 -3
  30. package/src/db.ts +153 -28
  31. package/src/integrations/agentdojo.ts +94 -12
  32. package/src/integrations/index.ts +7 -0
  33. package/src/mcp-platform.ts +494 -121
  34. package/src/providers.ts +12 -12
  35. package/src/routes/api/agent-utils.ts +59 -46
  36. package/src/routes/api/agents.ts +52 -1
  37. package/src/routes/api/integrations.ts +11 -5
  38. package/src/routes/api/mcp.ts +5 -4
  39. package/src/routes/api/meta-agent.ts +35 -1
  40. package/src/routes/api/projects.ts +3 -3
  41. package/src/routes/api/providers.ts +121 -30
  42. package/src/routes/api/skills.ts +2 -3
  43. package/src/routes/api/system.ts +8 -13
  44. package/src/server.ts +31 -32
  45. package/src/triggers/agentdojo.ts +2 -2
  46. package/src/web/App.tsx +18 -10
  47. package/src/web/components/activity/ActivityPage.tsx +241 -388
  48. package/src/web/components/agents/AgentCard.tsx +5 -13
  49. package/src/web/components/common/Icons.tsx +8 -0
  50. package/src/web/components/common/Select.tsx +4 -3
  51. package/src/web/components/dashboard/Dashboard.tsx +155 -30
  52. package/src/web/components/index.ts +1 -1
  53. package/src/web/components/layout/Sidebar.tsx +7 -1
  54. package/src/web/components/mcp/IntegrationsPanel.tsx +126 -35
  55. package/src/web/components/mcp/McpPage.tsx +10 -1
  56. package/src/web/components/meta-agent/MetaAgent.tsx +4 -2
  57. package/src/web/components/settings/SettingsPage.tsx +133 -48
  58. package/src/web/components/tasks/TasksPage.tsx +48 -16
  59. package/src/web/components/telemetry/TelemetryPage.tsx +184 -0
  60. package/src/web/components/threads/ThreadsPage.tsx +313 -0
  61. package/src/web/context/AuthContext.tsx +3 -3
  62. package/src/web/context/ProjectContext.tsx +3 -3
  63. package/src/web/context/TelemetryContext.tsx +24 -6
  64. package/src/web/context/index.ts +1 -1
  65. package/src/web/styles.css +20 -4
  66. package/src/web/types.ts +4 -3
  67. package/dist/ActivityPage.41nbye4r.js +0 -3
  68. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  69. package/dist/App.0sbax9et.js +0 -4
  70. package/dist/App.0ws427h8.js +0 -4
  71. package/dist/App.6q6bar8b.js +0 -4
  72. package/dist/App.80301vdb.js +0 -4
  73. package/dist/App.af2wg84v.js +0 -267
  74. package/dist/App.ca1rz1ph.js +0 -4
  75. package/dist/App.ensa6z0r.js +0 -4
  76. package/dist/App.f8g7tych.js +0 -13
  77. package/dist/App.mvtqv6qc.js +0 -20
  78. package/dist/App.ncgc9cxy.js +0 -4
  79. package/dist/App.p0fb1pds.js +0 -4
  80. package/dist/App.pmaq48sj.js +0 -4
  81. package/dist/App.yv87t9m5.js +0 -4
  82. package/dist/App.zjmfm8p6.js +0 -4
  83. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  84. package/dist/McpPage.y396h6fy.js +0 -3
  85. package/dist/SettingsPage.p1hc60gk.js +0 -3
  86. package/dist/SkillsPage.yj3xdsay.js +0 -3
  87. package/dist/TasksPage.sjv0khtv.js +0 -3
  88. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  89. package/dist/TestsPage.zzs4qfj8.js +0 -3
@@ -10,6 +10,12 @@ interface IntegrationApp {
10
10
  logo: string | null;
11
11
  categories: string[];
12
12
  authSchemes: string[];
13
+ providerSlug?: string;
14
+ credentialFields?: {
15
+ name: string;
16
+ description?: string;
17
+ required?: boolean;
18
+ }[];
13
19
  }
14
20
 
15
21
  interface ConnectedAccount {
@@ -68,9 +74,10 @@ export function IntegrationsPanel({
68
74
  const [error, setError] = useState<string | null>(null);
69
75
  // For auth method selection (when app supports both OAuth and API Key)
70
76
  const [authMethodModal, setAuthMethodModal] = useState<{ app: IntegrationApp } | null>(null);
71
- // For API Key modal
77
+ // For API Key / credential modal
72
78
  const [apiKeyModal, setApiKeyModal] = useState<{ app: IntegrationApp } | null>(null);
73
79
  const [apiKeyInput, setApiKeyInput] = useState("");
80
+ const [credentialInputs, setCredentialInputs] = useState<Record<string, string>>({});
74
81
  // For MCP config creation modal
75
82
  const [mcpConfigModal, setMcpConfigModal] = useState<{ app: IntegrationApp } | null>(null);
76
83
  const [mcpConfigName, setMcpConfigName] = useState("");
@@ -153,17 +160,18 @@ export function IntegrationsPanel({
153
160
  }, [pendingConnection, authFetch, providerId, projectId, fetchData, onConnectionComplete]);
154
161
 
155
162
  // Initiate connection
156
- const connectApp = async (app: IntegrationApp, apiKey?: string, forceOAuth?: boolean) => {
163
+ const connectApp = async (app: IntegrationApp, apiKey?: string, forceOAuth?: boolean, fields?: Record<string, string>) => {
157
164
  // If app supports multiple auth methods and user hasn't chosen, show choice
158
- if (hasMultipleAuthMethods(app) && !apiKey && !forceOAuth) {
165
+ if (hasMultipleAuthMethods(app) && !apiKey && !fields && !forceOAuth) {
159
166
  setAuthMethodModal({ app });
160
167
  return;
161
168
  }
162
169
 
163
- // If app supports API key (and user didn't choose OAuth), show API key modal
164
- if (supportsApiKey(app) && !apiKey && !forceOAuth) {
170
+ // If app supports API key (and user didn't choose OAuth), show credential modal
171
+ if (supportsApiKey(app) && !apiKey && !fields && !forceOAuth) {
165
172
  setApiKeyModal({ app });
166
173
  setApiKeyInput("");
174
+ setCredentialInputs({});
167
175
  return;
168
176
  }
169
177
 
@@ -173,7 +181,13 @@ export function IntegrationsPanel({
173
181
  try {
174
182
  // Build request body
175
183
  const body: any = { appSlug: app.slug };
176
- if (apiKey) {
184
+ if (fields && Object.keys(fields).length > 0) {
185
+ // Multi-field credentials
186
+ body.credentials = {
187
+ authScheme: "API_KEY",
188
+ fields,
189
+ };
190
+ } else if (apiKey) {
177
191
  body.credentials = {
178
192
  authScheme: "API_KEY",
179
193
  apiKey,
@@ -231,11 +245,21 @@ export function IntegrationsPanel({
231
245
  }
232
246
  };
233
247
 
234
- // Handle API key form submission
248
+ // Handle API key / credential form submission
235
249
  const handleApiKeySubmit = (e: React.FormEvent) => {
236
250
  e.preventDefault();
237
- if (!apiKeyModal || !apiKeyInput.trim()) return;
238
- connectApp(apiKeyModal.app, apiKeyInput.trim());
251
+ if (!apiKeyModal) return;
252
+ const hasFields = apiKeyModal.app.credentialFields && apiKeyModal.app.credentialFields.length > 0;
253
+ if (hasFields) {
254
+ // Check required fields are filled
255
+ const requiredFields = apiKeyModal.app.credentialFields!.filter(f => f.required !== false);
256
+ const allFilled = requiredFields.every(f => credentialInputs[f.name]?.trim());
257
+ if (!allFilled) return;
258
+ connectApp(apiKeyModal.app, undefined, false, credentialInputs);
259
+ } else {
260
+ if (!apiKeyInput.trim()) return;
261
+ connectApp(apiKeyModal.app, apiKeyInput.trim());
262
+ }
239
263
  };
240
264
 
241
265
  // Disconnect (called after confirmation)
@@ -291,6 +315,20 @@ export function IntegrationsPanel({
291
315
  return;
292
316
  }
293
317
 
318
+ // Auto-add the server locally
319
+ let autoAdded = false;
320
+ if (data.config?.id) {
321
+ try {
322
+ const addRes = await authFetch(
323
+ `/api/integrations/${providerId}/configs/${data.config.id}/add${projectParam}`,
324
+ { method: "POST" }
325
+ );
326
+ autoAdded = addRes.ok;
327
+ } catch {
328
+ // Non-fatal — server was still created on the provider
329
+ }
330
+ }
331
+
294
332
  setMcpConfigSuccess(mcpConfigName);
295
333
  onConnectionComplete?.();
296
334
  } catch (e) {
@@ -311,17 +349,23 @@ export function IntegrationsPanel({
311
349
  });
312
350
  };
313
351
 
314
- // Check if app is connected
315
- const isConnected = (appSlug: string) => {
352
+ // Check if app is connected (also matches via providerSlug for multi-toolkit providers)
353
+ const isConnected = (app: IntegrationApp) => {
316
354
  return connectedAccounts.some(
317
- (a) => a.appId === appSlug && a.status === "active"
355
+ (a) => a.status === "active" && (
356
+ a.appId === app.slug ||
357
+ (app.providerSlug && a.appId === app.providerSlug)
358
+ )
318
359
  );
319
360
  };
320
361
 
321
- // Get connection for app (prefer active account)
322
- const getConnection = (appSlug: string) => {
323
- return connectedAccounts.find((a) => a.appId === appSlug && a.status === "active")
324
- || connectedAccounts.find((a) => a.appId === appSlug);
362
+ // Get connection for app (prefer active account, also matches via providerSlug)
363
+ const getConnection = (app: IntegrationApp) => {
364
+ return connectedAccounts.find((a) => a.appId === app.slug && a.status === "active")
365
+ || (app.providerSlug && connectedAccounts.find((a) => a.appId === app.providerSlug && a.status === "active"))
366
+ || connectedAccounts.find((a) => a.appId === app.slug)
367
+ || (app.providerSlug && connectedAccounts.find((a) => a.appId === app.providerSlug))
368
+ || undefined;
325
369
  };
326
370
 
327
371
  // Filter apps
@@ -337,8 +381,8 @@ export function IntegrationsPanel({
337
381
  });
338
382
 
339
383
  // Group by connected/not connected
340
- const connectedApps = filteredApps.filter((app) => isConnected(app.slug));
341
- const availableApps = filteredApps.filter((app) => !isConnected(app.slug));
384
+ const connectedApps = filteredApps.filter((app) => isConnected(app));
385
+ const availableApps = filteredApps.filter((app) => !isConnected(app));
342
386
 
343
387
  if (loading) {
344
388
  return <div className="text-center py-8 text-[#666]">Loading apps...</div>;
@@ -369,6 +413,7 @@ export function IntegrationsPanel({
369
413
  setAuthMethodModal(null);
370
414
  setApiKeyModal({ app: authMethodModal.app });
371
415
  setApiKeyInput("");
416
+ setCredentialInputs({});
372
417
  }}
373
418
  className="w-full text-left p-3 bg-[#0a0a0a] hover:bg-[#1a1a1a] border border-[#333] hover:border-[#f97316] rounded-lg transition"
374
419
  >
@@ -400,7 +445,7 @@ export function IntegrationsPanel({
400
445
  </div>
401
446
  )}
402
447
 
403
- {/* API Key Modal */}
448
+ {/* API Key / Credentials Modal */}
404
449
  {apiKeyModal && (
405
450
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
406
451
  <div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-md mx-4">
@@ -414,18 +459,46 @@ export function IntegrationsPanel({
414
459
  )}
415
460
  <div>
416
461
  <h3 className="font-medium">Connect {apiKeyModal.app.name}</h3>
417
- <p className="text-xs text-[#666]">Enter your API key to connect</p>
462
+ <p className="text-xs text-[#666]">
463
+ {apiKeyModal.app.credentialFields?.length
464
+ ? "Enter your credentials to connect"
465
+ : "Enter your API key to connect"}
466
+ </p>
418
467
  </div>
419
468
  </div>
420
469
  <form onSubmit={handleApiKeySubmit}>
421
- <input
422
- type="password"
423
- value={apiKeyInput}
424
- onChange={(e) => setApiKeyInput(e.target.value)}
425
- placeholder="Enter API Key..."
426
- className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-2 mb-4 focus:outline-none focus:border-[#f97316]"
427
- autoFocus
428
- />
470
+ {apiKeyModal.app.credentialFields && apiKeyModal.app.credentialFields.length > 0 ? (
471
+ <div className="space-y-3 mb-4">
472
+ {apiKeyModal.app.credentialFields.map((field, idx) => (
473
+ <div key={field.name}>
474
+ <label className="block text-xs text-[#888] mb-1">
475
+ {field.name.replace(/([A-Z])/g, " $1").replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase()).trim()}
476
+ {field.required !== false && <span className="text-red-400 ml-0.5">*</span>}
477
+ </label>
478
+ {field.description && (
479
+ <p className="text-[10px] text-[#555] mb-1">{field.description}</p>
480
+ )}
481
+ <input
482
+ type="password"
483
+ value={credentialInputs[field.name] || ""}
484
+ onChange={(e) => setCredentialInputs(prev => ({ ...prev, [field.name]: e.target.value }))}
485
+ placeholder={`Enter ${field.name}...`}
486
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-2 focus:outline-none focus:border-[#f97316]"
487
+ autoFocus={idx === 0}
488
+ />
489
+ </div>
490
+ ))}
491
+ </div>
492
+ ) : (
493
+ <input
494
+ type="password"
495
+ value={apiKeyInput}
496
+ onChange={(e) => setApiKeyInput(e.target.value)}
497
+ placeholder="Enter API Key..."
498
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-2 mb-4 focus:outline-none focus:border-[#f97316]"
499
+ autoFocus
500
+ />
501
+ )}
429
502
  <div className="flex gap-2">
430
503
  <button
431
504
  type="button"
@@ -436,7 +509,12 @@ export function IntegrationsPanel({
436
509
  </button>
437
510
  <button
438
511
  type="submit"
439
- disabled={!apiKeyInput.trim() || connecting === apiKeyModal.app.slug}
512
+ disabled={
513
+ connecting === apiKeyModal.app.slug ||
514
+ (apiKeyModal.app.credentialFields?.length
515
+ ? !apiKeyModal.app.credentialFields.filter(f => f.required !== false).every(f => credentialInputs[f.name]?.trim())
516
+ : !apiKeyInput.trim())
517
+ }
440
518
  className="flex-1 text-sm bg-[#f97316] hover:bg-[#ea580c] text-white px-4 py-2 rounded transition disabled:opacity-50"
441
519
  >
442
520
  {connecting === apiKeyModal.app.slug ? "Connecting..." : "Connect"}
@@ -459,10 +537,7 @@ export function IntegrationsPanel({
459
537
  </div>
460
538
  <h3 className="font-medium text-lg">MCP Config Created!</h3>
461
539
  <p className="text-sm text-[#888] mt-2">
462
- "{mcpConfigSuccess}" has been created successfully.
463
- </p>
464
- <p className="text-xs text-[#666] mt-2">
465
- You can now add it to your agents from the MCP Configs tab.
540
+ "{mcpConfigSuccess}" has been created and added to your servers.
466
541
  </p>
467
542
  </div>
468
543
  <button
@@ -589,14 +664,19 @@ export function IntegrationsPanel({
589
664
  <AppCard
590
665
  key={app.id}
591
666
  app={app}
592
- connection={getConnection(app.slug)}
667
+ connection={getConnection(app)}
593
668
  onConnect={() => connectApp(app)}
594
669
  onDisconnect={() => {
595
- const conn = getConnection(app.slug);
670
+ const conn = getConnection(app);
596
671
  if (conn) handleDisconnect(conn);
597
672
  }}
598
673
  onCreateMcpConfig={hideMcpConfig ? undefined : () => openMcpConfigModal(app)}
599
674
  onBrowseTriggers={onBrowseTriggers ? () => onBrowseTriggers(app.slug) : undefined}
675
+ onUpdateKey={supportsApiKey(app) ? () => {
676
+ setApiKeyModal({ app });
677
+ setApiKeyInput("");
678
+ setCredentialInputs({});
679
+ } : undefined}
600
680
  connecting={connecting === app.slug}
601
681
  />
602
682
  ))}
@@ -643,6 +723,7 @@ function AppCard({
643
723
  onDisconnect,
644
724
  onCreateMcpConfig,
645
725
  onBrowseTriggers,
726
+ onUpdateKey,
646
727
  connecting,
647
728
  }: {
648
729
  app: IntegrationApp;
@@ -651,6 +732,7 @@ function AppCard({
651
732
  onDisconnect?: () => void;
652
733
  onCreateMcpConfig?: () => void;
653
734
  onBrowseTriggers?: () => void;
735
+ onUpdateKey?: () => void;
654
736
  connecting: boolean;
655
737
  }) {
656
738
  const isConnected = connection?.status === "active";
@@ -739,6 +821,15 @@ function AppCard({
739
821
  Browse Triggers
740
822
  </button>
741
823
  )}
824
+ {onUpdateKey && (
825
+ <button
826
+ onClick={onUpdateKey}
827
+ className="text-xs text-[#666] hover:text-[#f97316] transition px-2"
828
+ title="Update API Key"
829
+ >
830
+ Key
831
+ </button>
832
+ )}
742
833
  {onDisconnect && (
743
834
  <button
744
835
  onClick={onDisconnect}
@@ -251,7 +251,16 @@ export function McpPage() {
251
251
  onStart={() => startServer(server.id)}
252
252
  onStop={() => stopServer(server.id)}
253
253
  onDelete={() => deleteServer(server.id)}
254
- onEdit={() => setEditingServer(server)}
254
+ onEdit={async () => {
255
+ // Fetch full server details (with decrypted env/headers) for editing
256
+ try {
257
+ const res = await authFetch(`/api/mcp/servers/${server.id}`);
258
+ const data = await res.json();
259
+ setEditingServer(data.server || server);
260
+ } catch {
261
+ setEditingServer(server);
262
+ }
263
+ }}
255
264
  />
256
265
  );
257
266
  })}
@@ -1,6 +1,6 @@
1
- import React, { useState, useEffect, useMemo, createContext, useContext, type ReactNode } from "react";
1
+ import React, { useState, useEffect, useMemo, useCallback, createContext, useContext, type ReactNode } from "react";
2
2
  import { Chat } from "@apteva/apteva-kit";
3
- import { useAuth, useProjects } from "../../context";
3
+ import { useAuth, useProjects, useTriggerRefresh } from "../../context";
4
4
 
5
5
  interface MetaAgentStatus {
6
6
  enabled: boolean;
@@ -130,6 +130,7 @@ export function MetaAgentButton() {
130
130
  export function MetaAgentPanel() {
131
131
  const ctx = useMetaAgent();
132
132
  const { currentProjectId, currentProject } = useProjects();
133
+ const triggerRefresh = useTriggerRefresh();
133
134
  if (!ctx?.isAvailable || !ctx.isOpen) return null;
134
135
 
135
136
  const { agent, isRunning, error, isStarting, startAgent, close } = ctx;
@@ -181,6 +182,7 @@ export function MetaAgentPanel() {
181
182
  variant="terminal"
182
183
  showHeader={false}
183
184
  context={chatContext}
185
+ onToolResult={triggerRefresh}
184
186
  />
185
187
  ) : (
186
188
  <div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
@@ -406,7 +406,7 @@ function ProvidersSettings() {
406
406
 
407
407
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
408
408
  {browserProviders.map(provider => (
409
- <ProviderKeyCard
409
+ <IntegrationKeyCard
410
410
  key={provider.id}
411
411
  provider={provider}
412
412
  isEditing={selectedProvider === provider.id}
@@ -423,14 +423,14 @@ function ProvidersSettings() {
423
423
  onCancelEdit={() => {
424
424
  setSelectedProvider(null);
425
425
  setApiKey("");
426
- setExtraField("");
427
426
  setError(null);
428
427
  }}
429
428
  onApiKeyChange={setApiKey}
430
429
  onSave={saveKey}
431
430
  onDelete={() => deleteKey(provider.id)}
432
- extraField={extraField}
433
- onExtraFieldChange={setExtraField}
431
+ projectsEnabled={projectsEnabled}
432
+ projects={projects}
433
+ onRefresh={fetchProviders}
434
434
  />
435
435
  ))}
436
436
  </div>
@@ -956,20 +956,47 @@ function ProviderKeyCard({
956
956
  onExtraFieldChange,
957
957
  }: ProviderKeyCardProps) {
958
958
  const isOllama = provider.id === "ollama";
959
- const isUrlBased = isOllama || provider.id === "browserengine" || provider.id === "chrome";
959
+ const isCDP = provider.id === "cdp";
960
+ const isUrlBased = isOllama || isCDP;
960
961
  const isBrowser = provider.type === "browser";
961
962
  const isMultiField = provider.id === "browserbase";
962
- const [ollamaStatus, setOllamaStatus] = React.useState<{ connected: boolean; modelCount?: number } | null>(null);
963
+ const [ollamaStatus, setOllamaStatus] = React.useState<{ connected: boolean; modelCount?: number; isDocker?: boolean } | null>(null);
964
+ const [installing, setInstalling] = React.useState(false);
965
+ const [installResult, setInstallResult] = React.useState<{ success: boolean; message: string } | null>(null);
966
+
967
+ // Check Ollama status when configured or after install
968
+ const checkOllamaStatus = React.useCallback(() => {
969
+ fetch("/api/providers/ollama/status")
970
+ .then(res => res.json())
971
+ .then(data => setOllamaStatus({ connected: data.connected, modelCount: data.modelCount, isDocker: data.isDocker }))
972
+ .catch(() => setOllamaStatus({ connected: false }));
973
+ }, []);
963
974
 
964
- // Check Ollama status when configured
965
975
  React.useEffect(() => {
966
- if (isOllama && provider.hasKey) {
967
- fetch("/api/providers/ollama/status")
968
- .then(res => res.json())
969
- .then(data => setOllamaStatus({ connected: data.connected, modelCount: data.modelCount }))
970
- .catch(() => setOllamaStatus({ connected: false }));
976
+ if (isOllama) {
977
+ checkOllamaStatus();
978
+ }
979
+ }, [isOllama, provider.hasKey, checkOllamaStatus]);
980
+
981
+ const handleInstallOllama = async () => {
982
+ setInstalling(true);
983
+ setInstallResult(null);
984
+ try {
985
+ const res = await fetch("/api/providers/ollama/install", { method: "POST" });
986
+ const data = await res.json();
987
+ if (data.success) {
988
+ setInstallResult({ success: true, message: data.message });
989
+ // Auto-save the default URL and refresh status
990
+ checkOllamaStatus();
991
+ } else {
992
+ setInstallResult({ success: false, message: data.error || "Installation failed" });
993
+ }
994
+ } catch {
995
+ setInstallResult({ success: false, message: "Failed to connect to server" });
996
+ } finally {
997
+ setInstalling(false);
971
998
  }
972
- }, [isOllama, provider.hasKey]);
999
+ };
973
1000
 
974
1001
  return (
975
1002
  <div className={`bg-[#111] border rounded-lg p-4 ${
@@ -1049,22 +1076,17 @@ function ProviderKeyCard({
1049
1076
  onChange={e => onApiKeyChange(e.target.value)}
1050
1077
  placeholder={isOllama
1051
1078
  ? "http://localhost:11434"
1052
- : provider.id === "browserengine"
1053
- ? "http://localhost:8098"
1054
- : provider.id === "chrome"
1055
- ? "http://localhost:9222"
1056
- : provider.hasKey ? "Enter new API key..." : "Enter API key..."}
1079
+ : isCDP ? "ws://localhost:9222"
1080
+ : provider.hasKey ? "Enter new API key..." : "Enter API key..."}
1057
1081
  autoFocus
1058
1082
  className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1059
1083
  />
1060
1084
  )}
1061
1085
  {isUrlBased && (
1062
1086
  <p className="text-xs text-[#666]">
1063
- {isOllama
1064
- ? "Enter your Ollama server URL. Default is http://localhost:11434"
1065
- : provider.id === "browserengine"
1066
- ? "Enter your BrowserEngine service URL (e.g., http://localhost:8098)"
1067
- : "Enter your Chrome DevTools URL (e.g., http://localhost:9222)"}
1087
+ {isCDP
1088
+ ? "Enter the CDP URL of your browser (e.g., ws://localhost:9222)"
1089
+ : "Enter your Ollama server URL. Default is http://localhost:11434"}
1068
1090
  </p>
1069
1091
  )}
1070
1092
  {error && <p className="text-red-400 text-sm">{error}</p>}
@@ -1086,7 +1108,24 @@ function ProviderKeyCard({
1086
1108
  </div>
1087
1109
  </div>
1088
1110
  ) : provider.hasKey ? (
1089
- <div className="flex items-center justify-between">
1111
+ <div>
1112
+ {isOllama && ollamaStatus && !ollamaStatus.connected && !ollamaStatus.isDocker && (
1113
+ <div className="mb-3">
1114
+ <button
1115
+ onClick={handleInstallOllama}
1116
+ disabled={installing}
1117
+ className="w-full px-3 py-1.5 bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 rounded text-sm font-medium transition disabled:opacity-50 disabled:cursor-wait"
1118
+ >
1119
+ {installing ? "Starting Ollama..." : "Start Ollama"}
1120
+ </button>
1121
+ {installResult && (
1122
+ <p className={`text-xs mt-1.5 ${installResult.success ? "text-green-400" : "text-red-400"}`}>
1123
+ {installResult.message}
1124
+ </p>
1125
+ )}
1126
+ </div>
1127
+ )}
1128
+ <div className="flex items-center justify-between">
1090
1129
  {provider.docsUrl ? (
1091
1130
  <a
1092
1131
  href={provider.docsUrl}
@@ -1094,7 +1133,7 @@ function ProviderKeyCard({
1094
1133
  rel="noopener noreferrer"
1095
1134
  className="text-sm text-[#3b82f6] hover:underline"
1096
1135
  >
1097
- {isOllama ? "Download Ollama" : "View docs"}
1136
+ {isOllama ? "Ollama docs" : "View docs"}
1098
1137
  </a>
1099
1138
  ) : (
1100
1139
  <span />
@@ -1113,27 +1152,46 @@ function ProviderKeyCard({
1113
1152
  Remove
1114
1153
  </button>
1115
1154
  </div>
1155
+ </div>
1116
1156
  </div>
1117
1157
  ) : (
1118
- <div className="flex items-center justify-between">
1119
- {provider.docsUrl ? (
1120
- <a
1121
- href={provider.docsUrl}
1122
- target="_blank"
1123
- rel="noopener noreferrer"
1124
- className="text-sm text-[#3b82f6] hover:underline"
1125
- >
1126
- {isOllama ? "Download Ollama" : isBrowser ? "View docs" : "Get API key"}
1127
- </a>
1128
- ) : (
1129
- <span />
1158
+ <div>
1159
+ {isOllama && !ollamaStatus?.isDocker && (
1160
+ <div className="mb-3">
1161
+ <button
1162
+ onClick={handleInstallOllama}
1163
+ disabled={installing}
1164
+ className="w-full px-3 py-2 bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30 rounded text-sm font-medium transition disabled:opacity-50 disabled:cursor-wait"
1165
+ >
1166
+ {installing ? "Installing Ollama..." : ollamaStatus?.connected ? "Ollama Running" : "Install Ollama"}
1167
+ </button>
1168
+ {installResult && (
1169
+ <p className={`text-xs mt-1.5 ${installResult.success ? "text-green-400" : "text-red-400"}`}>
1170
+ {installResult.message}
1171
+ </p>
1172
+ )}
1173
+ </div>
1130
1174
  )}
1131
- <button
1132
- onClick={onStartEdit}
1133
- className="text-sm text-[#f97316] hover:text-[#fb923c]"
1134
- >
1135
- {isUrlBased ? "Configure" : "+ Add key"}
1136
- </button>
1175
+ <div className="flex items-center justify-between">
1176
+ {provider.docsUrl ? (
1177
+ <a
1178
+ href={provider.docsUrl}
1179
+ target="_blank"
1180
+ rel="noopener noreferrer"
1181
+ className="text-sm text-[#3b82f6] hover:underline"
1182
+ >
1183
+ {isOllama ? "Manual install" : isBrowser ? "View docs" : "Get API key"}
1184
+ </a>
1185
+ ) : (
1186
+ <span />
1187
+ )}
1188
+ <button
1189
+ onClick={onStartEdit}
1190
+ className="text-sm text-[#f97316] hover:text-[#fb923c]"
1191
+ >
1192
+ {isUrlBased ? "Configure" : "+ Add key"}
1193
+ </button>
1194
+ </div>
1137
1195
  </div>
1138
1196
  )}
1139
1197
  </div>
@@ -1180,8 +1238,11 @@ function IntegrationKeyCard({
1180
1238
  const [expanded, setExpanded] = useState(false);
1181
1239
  const [localError, setLocalError] = useState<string | null>(null);
1182
1240
  const [localSaving, setLocalSaving] = useState(false);
1241
+ const [bbProjectId, setBbProjectId] = useState(""); // Browserbase project ID (their internal ID)
1183
1242
  const { confirm, ConfirmDialog } = useConfirm();
1184
1243
 
1244
+ const isBrowserbase = provider.id === "browserbase";
1245
+
1185
1246
  // Fetch all keys for this provider
1186
1247
  const fetchKeys = async () => {
1187
1248
  try {
@@ -1212,12 +1273,18 @@ function IntegrationKeyCard({
1212
1273
  setLocalSaving(true);
1213
1274
  setLocalError(null);
1214
1275
 
1276
+ // For Browserbase, combine API key + BB project ID into JSON
1277
+ let keyToSave = apiKey;
1278
+ if (isBrowserbase && bbProjectId) {
1279
+ keyToSave = JSON.stringify({ api_key: apiKey, project_id: bbProjectId });
1280
+ }
1281
+
1215
1282
  try {
1216
1283
  const res = await authFetch(`/api/keys/${provider.id}`, {
1217
1284
  method: "POST",
1218
1285
  headers: { "Content-Type": "application/json" },
1219
1286
  body: JSON.stringify({
1220
- key: apiKey,
1287
+ key: keyToSave,
1221
1288
  project_id: selectedProjectId || null,
1222
1289
  }),
1223
1290
  });
@@ -1226,6 +1293,7 @@ function IntegrationKeyCard({
1226
1293
 
1227
1294
  if (res.ok) {
1228
1295
  onApiKeyChange("");
1296
+ setBbProjectId("");
1229
1297
  setSelectedProjectId("");
1230
1298
  onCancelEdit();
1231
1299
  fetchKeys();
@@ -1288,10 +1356,10 @@ function IntegrationKeyCard({
1288
1356
  {isEditing ? (
1289
1357
  <div className="space-y-3">
1290
1358
  <input
1291
- type="password"
1359
+ type={inputType}
1292
1360
  value={apiKey}
1293
1361
  onChange={e => onApiKeyChange(e.target.value)}
1294
- placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
1362
+ placeholder={provider.hasKey ? `Enter new ${isUrlBased ? "URL" : "API key"}...` : inputPlaceholder}
1295
1363
  autoFocus
1296
1364
  className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1297
1365
  />
@@ -1361,6 +1429,13 @@ function IntegrationKeyCard({
1361
1429
  );
1362
1430
  }
1363
1431
 
1432
+ // Determine input type and placeholder based on provider
1433
+ const isUrlBased = provider.isLocal;
1434
+ const inputType = isUrlBased ? "text" : "password";
1435
+ const inputPlaceholder = isUrlBased
1436
+ ? (provider.id === "cdp" ? "ws://localhost:9222" : "http://localhost:11434")
1437
+ : "Enter API key...";
1438
+
1364
1439
  // Enhanced view with project support
1365
1440
  return (
1366
1441
  <>
@@ -1441,14 +1516,24 @@ function IntegrationKeyCard({
1441
1516
  {isEditing ? (
1442
1517
  <div className="space-y-3">
1443
1518
  <input
1444
- type="password"
1519
+ type={inputType}
1445
1520
  value={apiKey}
1446
1521
  onChange={e => onApiKeyChange(e.target.value)}
1447
- placeholder="Enter API key..."
1522
+ placeholder={inputPlaceholder}
1448
1523
  autoFocus
1449
1524
  className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
1450
1525
  />
1451
1526
 
1527
+ {isBrowserbase && (
1528
+ <input
1529
+ type="text"
1530
+ value={bbProjectId}
1531
+ onChange={e => setBbProjectId(e.target.value)}
1532
+ placeholder="Browserbase Project ID (optional)"
1533
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316] text-sm"
1534
+ />
1535
+ )}
1536
+
1452
1537
  <Select
1453
1538
  value={selectedProjectId}
1454
1539
  onChange={setSelectedProjectId}