@swarmclawai/swarmclaw 0.2.0

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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,542 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from '@/lib/provider-config'
6
+ import { api } from '@/lib/api-client'
7
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
8
+ import { AiGenBlock } from '@/components/shared/ai-gen-block'
9
+ import { toast } from 'sonner'
10
+
11
+ export function ProviderSheet() {
12
+ const open = useAppStore((s) => s.providerSheetOpen)
13
+ const setOpen = useAppStore((s) => s.setProviderSheetOpen)
14
+ const editingId = useAppStore((s) => s.editingProviderId)
15
+ const setEditingId = useAppStore((s) => s.setEditingProviderId)
16
+ const providerConfigs = useAppStore((s) => s.providerConfigs)
17
+ const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
18
+ const providers = useAppStore((s) => s.providers)
19
+ const loadProviders = useAppStore((s) => s.loadProviders)
20
+ const credentials = useAppStore((s) => s.credentials)
21
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
22
+
23
+ const [name, setName] = useState('')
24
+ const [baseUrl, setBaseUrl] = useState('')
25
+ const [models, setModels] = useState('')
26
+ const [requiresApiKey, setRequiresApiKey] = useState(true)
27
+ const [credentialId, setCredentialId] = useState<string | null>(null)
28
+ const [isEnabled, setIsEnabled] = useState(true)
29
+ const [addingKey, setAddingKey] = useState(false)
30
+ const [newKeyName, setNewKeyName] = useState('')
31
+ const [newKeyValue, setNewKeyValue] = useState('')
32
+ const [savingKey, setSavingKey] = useState(false)
33
+ const [newModel, setNewModel] = useState('')
34
+
35
+ // Test connection state
36
+ const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
37
+ const [testMessage, setTestMessage] = useState('')
38
+
39
+ // Ollama local models
40
+ const [localModels, setLocalModels] = useState<string[]>([])
41
+ const [localLoading, setLocalLoading] = useState(false)
42
+ const [localError, setLocalError] = useState('')
43
+
44
+ // AI generation state
45
+ const [aiPrompt, setAiPrompt] = useState('')
46
+ const [generating, setGenerating] = useState(false)
47
+ const [generated, setGenerated] = useState(false)
48
+ const [genError, setGenError] = useState('')
49
+ const appSettings = useAppStore((s) => s.appSettings)
50
+ const loadSettings = useAppStore((s) => s.loadSettings)
51
+
52
+ // Find editing provider in custom configs OR built-in list
53
+ const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
54
+ const editingBuiltin = editingId ? providers.find((p) => p.id === editingId) : null
55
+ const isBuiltin = !!editingBuiltin && !editingCustom
56
+ const editing = editingCustom || editingBuiltin
57
+
58
+ const handleGenerate = async () => {
59
+ if (!aiPrompt.trim()) return
60
+ setGenerating(true)
61
+ setGenError('')
62
+ try {
63
+ const result = await api<{ name?: string; baseUrl?: string; models?: string; requiresApiKey?: boolean; error?: string }>('POST', '/generate', { type: 'provider', prompt: aiPrompt })
64
+ if (result.error) {
65
+ setGenError(result.error)
66
+ } else if (result.name || result.baseUrl) {
67
+ if (result.name) setName(result.name)
68
+ if (result.baseUrl) setBaseUrl(result.baseUrl)
69
+ if (result.models) setModels(result.models)
70
+ if (result.requiresApiKey !== undefined) setRequiresApiKey(result.requiresApiKey)
71
+ setGenerated(true)
72
+ } else {
73
+ setGenError('AI returned empty response — try again')
74
+ }
75
+ } catch (err: unknown) {
76
+ setGenError(err instanceof Error ? err.message : 'Generation failed')
77
+ }
78
+ setGenerating(false)
79
+ }
80
+
81
+ useEffect(() => {
82
+ if (open) {
83
+ loadCredentials()
84
+ loadSettings()
85
+ setAiPrompt('')
86
+ setGenerating(false)
87
+ setGenerated(false)
88
+ setGenError('')
89
+ setNewModel('')
90
+ setLocalModels([])
91
+ setLocalError('')
92
+ setTestStatus('idle')
93
+ setTestMessage('')
94
+ if (editingCustom) {
95
+ setName(editingCustom.name)
96
+ setBaseUrl(editingCustom.baseUrl || '')
97
+ setModels(editingCustom.models.join(', '))
98
+ setRequiresApiKey(editingCustom.requiresApiKey)
99
+ setCredentialId(editingCustom.credentialId || null)
100
+ setIsEnabled(editingCustom.isEnabled)
101
+ } else if (editingBuiltin) {
102
+ setName(editingBuiltin.name)
103
+ setBaseUrl(editingBuiltin.defaultEndpoint || '')
104
+ setModels(editingBuiltin.models.join(', '))
105
+ setRequiresApiKey(editingBuiltin.requiresApiKey)
106
+ // Default to existing credential for this provider
107
+ const existingCred = Object.values(credentials).find((c) => c.provider === editingBuiltin.id)
108
+ setCredentialId(existingCred?.id || null)
109
+ setIsEnabled(true)
110
+ } else {
111
+ setName('')
112
+ setBaseUrl('')
113
+ setModels('')
114
+ setRequiresApiKey(true)
115
+ setCredentialId(null)
116
+ setIsEnabled(true)
117
+ }
118
+ }
119
+ }, [open, editingId])
120
+
121
+ // Fetch local Ollama models when editing Ollama provider
122
+ useEffect(() => {
123
+ if (!open || editingId !== 'ollama') return
124
+ setLocalLoading(true)
125
+ const endpoint = baseUrl || 'http://localhost:11434'
126
+ api<{ models: { name: string }[]; error?: string }>('GET', `/providers/ollama?endpoint=${encodeURIComponent(endpoint)}`)
127
+ .then((res) => {
128
+ if (res.error) {
129
+ setLocalError(res.error)
130
+ setLocalModels([])
131
+ } else {
132
+ setLocalModels(res.models.map((m) => m.name))
133
+ }
134
+ })
135
+ .catch(() => {
136
+ setLocalError('Failed to connect')
137
+ setLocalModels([])
138
+ })
139
+ .finally(() => setLocalLoading(false))
140
+ }, [open, editingId, baseUrl])
141
+
142
+ // Reset test status when connection params change
143
+ useEffect(() => {
144
+ setTestStatus('idle')
145
+ setTestMessage('')
146
+ }, [credentialId, baseUrl])
147
+
148
+ const handleTestConnection = async () => {
149
+ setTestStatus('testing')
150
+ setTestMessage('')
151
+ try {
152
+ const result = await api<{ ok: boolean; message: string }>('POST', '/setup/check-provider', {
153
+ provider: editingId || 'custom',
154
+ credentialId,
155
+ endpoint: baseUrl,
156
+ })
157
+ if (result.ok) {
158
+ setTestStatus('pass')
159
+ setTestMessage(result.message)
160
+ } else {
161
+ setTestStatus('fail')
162
+ setTestMessage(result.message)
163
+ }
164
+ } catch (err: unknown) {
165
+ setTestStatus('fail')
166
+ setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
167
+ }
168
+ }
169
+
170
+ const onClose = () => {
171
+ setOpen(false)
172
+ setEditingId(null)
173
+ }
174
+
175
+ const handleSave = async () => {
176
+ if (isBuiltin) {
177
+ // Save model overrides for built-in providers
178
+ const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
179
+ await api('PUT', `/providers/${editingId}/models`, { models: modelList })
180
+ await loadProviders()
181
+ onClose()
182
+ return
183
+ }
184
+ const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
185
+ const data = {
186
+ name: name.trim() || 'Custom Provider',
187
+ baseUrl: baseUrl.trim(),
188
+ models: modelList,
189
+ requiresApiKey,
190
+ credentialId,
191
+ isEnabled,
192
+ }
193
+ if (editingCustom) {
194
+ await updateProviderConfig(editingCustom.id, data)
195
+ } else {
196
+ await createProviderConfig(data)
197
+ }
198
+ await loadProviderConfigs()
199
+ onClose()
200
+ }
201
+
202
+ const handleDelete = async () => {
203
+ if (editingCustom) {
204
+ await deleteProviderConfig(editingCustom.id)
205
+ await loadProviderConfigs()
206
+ onClose()
207
+ }
208
+ }
209
+
210
+ const handleResetModels = async () => {
211
+ if (isBuiltin && editingId) {
212
+ await api('DELETE', `/providers/${editingId}/models`)
213
+ await loadProviders()
214
+ // Re-read the reset models
215
+ const updated = providers.find((p) => p.id === editingId)
216
+ if (updated) setModels(updated.models.join(', '))
217
+ }
218
+ }
219
+
220
+ const handleAddModel = () => {
221
+ if (!newModel.trim()) return
222
+ const current = models ? models + ', ' + newModel.trim() : newModel.trim()
223
+ setModels(current)
224
+ setNewModel('')
225
+ }
226
+
227
+ const handleRemoveModel = (index: number) => {
228
+ const list = models.split(',').map((m) => m.trim()).filter(Boolean)
229
+ list.splice(index, 1)
230
+ setModels(list.join(', '))
231
+ }
232
+
233
+ const credList = Object.values(credentials)
234
+ const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
235
+ const isNew = !editing
236
+ const showApiKey = isBuiltin ? editingBuiltin?.requiresApiKey || editingBuiltin?.optionalApiKey : requiresApiKey
237
+
238
+ const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
239
+
240
+ return (
241
+ <BottomSheet open={open} onClose={onClose} wide>
242
+ <div className="mb-10">
243
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
244
+ {isBuiltin ? editing?.name : editing ? 'Edit Provider' : 'New Provider'}
245
+ </h2>
246
+ <p className="text-[14px] text-text-3">
247
+ {isBuiltin ? 'Manage models and API key for this built-in provider' : 'Add an OpenAI-compatible provider (OpenRouter, Together, Groq, etc.)'}
248
+ </p>
249
+ </div>
250
+
251
+ {/* AI Generation — only for new custom providers */}
252
+ {isNew && <AiGenBlock
253
+ aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
254
+ generating={generating} generated={generated} genError={genError}
255
+ onGenerate={handleGenerate} appSettings={appSettings}
256
+ placeholder='Name a provider, e.g. "Groq", "Together AI", "z.ai", "DeepSeek"'
257
+ />}
258
+
259
+ {/* Name */}
260
+ <div className="mb-8">
261
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
262
+ <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. OpenRouter"
263
+ disabled={isBuiltin} className={`${inputClass} ${isBuiltin ? 'opacity-50' : ''}`} style={{ fontFamily: 'inherit' }} />
264
+ </div>
265
+
266
+ {/* Base URL — for custom providers and built-ins with endpoints (Ollama, OpenClaw) */}
267
+ {(!isBuiltin || editingBuiltin?.requiresEndpoint) && (
268
+ <div className="mb-8">
269
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
270
+ {isBuiltin ? 'Endpoint' : 'Base URL'}
271
+ </label>
272
+ <input type="text" value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)}
273
+ placeholder={editingBuiltin?.defaultEndpoint || 'https://openrouter.ai/api/v1'}
274
+ className={`${inputClass} font-mono text-[14px]`} />
275
+ <p className="text-[11px] text-text-3/70 mt-2">
276
+ {isBuiltin ? `Default: ${editingBuiltin?.defaultEndpoint || 'none'}` : 'OpenAI-compatible API endpoint (without /chat/completions)'}
277
+ </p>
278
+ </div>
279
+ )}
280
+
281
+ {/* Models — chip editor for built-in, textarea for custom */}
282
+ <div className="mb-8">
283
+ <div className="flex items-center justify-between mb-3">
284
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Models</label>
285
+ {isBuiltin && (
286
+ <button onClick={handleResetModels}
287
+ className="text-[11px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none"
288
+ style={{ fontFamily: 'inherit' }}>
289
+ Reset to defaults
290
+ </button>
291
+ )}
292
+ </div>
293
+
294
+ {isBuiltin ? (
295
+ <>
296
+ <div className="flex flex-wrap gap-1.5 mb-3">
297
+ {modelList.map((model, i) => {
298
+ const isLocal = editingId === 'ollama' && localModels.includes(model)
299
+ return (
300
+ <div key={`${model}-${i}`} className={`group/model flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border
301
+ ${isLocal ? 'bg-emerald-500/[0.08] border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'}`}>
302
+ <span className="text-[12px] text-text-2 font-mono">{model}</span>
303
+ {isLocal && (
304
+ <span className="text-[9px] font-600 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 text-emerald-400 uppercase tracking-wider">local</span>
305
+ )}
306
+ <button
307
+ onClick={() => handleRemoveModel(i)}
308
+ className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] text-text-3
309
+ opacity-0 group-hover/model:opacity-100 hover:bg-red-500/20 hover:text-red-400
310
+ transition-all cursor-pointer bg-transparent border-none"
311
+ >
312
+ &times;
313
+ </button>
314
+ </div>
315
+ )
316
+ })}
317
+ </div>
318
+
319
+ {/* Ollama: show available local models not yet in the list */}
320
+ {editingId === 'ollama' && !localLoading && localModels.length > 0 && (() => {
321
+ const missing = localModels.filter((m) => !modelList.includes(m))
322
+ if (missing.length === 0) return null
323
+ return (
324
+ <div className="mb-3">
325
+ <p className="text-[11px] text-text-3/60 mb-2">Available locally — click to add:</p>
326
+ <div className="flex flex-wrap gap-1.5">
327
+ {missing.map((m) => (
328
+ <button key={m} onClick={() => { setModels(models ? models + ', ' + m : m) }}
329
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] bg-emerald-500/[0.05] border border-emerald-500/15
330
+ hover:bg-emerald-500/10 transition-all cursor-pointer text-[12px] text-emerald-300/80 font-mono"
331
+ style={{ fontFamily: 'inherit' }}>
332
+ <span>+</span> {m}
333
+ </button>
334
+ ))}
335
+ </div>
336
+ </div>
337
+ )
338
+ })()}
339
+
340
+ {editingId === 'ollama' && localLoading && (
341
+ <p className="text-[11px] text-text-3/70 mb-3">Checking local Ollama instance...</p>
342
+ )}
343
+ {editingId === 'ollama' && localError && (
344
+ <p className="text-[11px] text-amber-400/60 mb-3">{localError}</p>
345
+ )}
346
+
347
+ <div className="flex gap-2">
348
+ <input
349
+ type="text"
350
+ value={newModel}
351
+ onChange={(e) => setNewModel(e.target.value)}
352
+ placeholder="Add model ID..."
353
+ className={`${inputClass} flex-1 font-mono text-[14px]`}
354
+ style={{ fontFamily: 'inherit' }}
355
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddModel() } }}
356
+ />
357
+ <button
358
+ onClick={handleAddModel}
359
+ disabled={!newModel.trim()}
360
+ className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600
361
+ cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
362
+ style={{ fontFamily: 'inherit' }}
363
+ >
364
+ Add
365
+ </button>
366
+ </div>
367
+ </>
368
+ ) : (
369
+ <>
370
+ <textarea
371
+ value={models}
372
+ onChange={(e) => setModels(e.target.value)}
373
+ placeholder="model-1, model-2, model-3"
374
+ rows={3}
375
+ className={`${inputClass} resize-y min-h-[80px] font-mono text-[14px]`}
376
+ style={{ fontFamily: 'inherit' }}
377
+ />
378
+ <p className="text-[11px] text-text-3/70 mt-2">Comma-separated model IDs</p>
379
+ </>
380
+ )}
381
+ </div>
382
+
383
+ {/* Requires API Key toggle — only for custom */}
384
+ {!isBuiltin && (
385
+ <div className="mb-8">
386
+ <label className="flex items-center gap-3 cursor-pointer">
387
+ <div
388
+ onClick={() => setRequiresApiKey(!requiresApiKey)}
389
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
390
+ ${requiresApiKey ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
391
+ >
392
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
393
+ ${requiresApiKey ? 'left-[22px]' : 'left-0.5'}`} />
394
+ </div>
395
+ <span className="font-display text-[14px] font-600 text-text-2">Requires API Key</span>
396
+ </label>
397
+ </div>
398
+ )}
399
+
400
+ {/* API Key section */}
401
+ {showApiKey && (
402
+ <div className="mb-8">
403
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
404
+ {isBuiltin ? 'API Key' : 'Linked API Key'}
405
+ {isBuiltin && editingBuiltin?.optionalApiKey && !editingBuiltin?.requiresApiKey && (
406
+ <span className="normal-case tracking-normal font-normal text-text-3 ml-1">(optional)</span>
407
+ )}
408
+ </label>
409
+ {credList.length > 0 && !addingKey ? (
410
+ <div className="flex gap-2">
411
+ <select value={credentialId || ''} onChange={(e) => {
412
+ if (e.target.value === '__add__') {
413
+ setAddingKey(true)
414
+ setNewKeyName('')
415
+ setNewKeyValue('')
416
+ } else {
417
+ setCredentialId(e.target.value || null)
418
+ }
419
+ }} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
420
+ <option value="">Select a key...</option>
421
+ {credList.map((c) => (
422
+ <option key={c.id} value={c.id}>{c.name} ({c.provider})</option>
423
+ ))}
424
+ <option value="__add__">+ Add new key...</option>
425
+ </select>
426
+ <button
427
+ type="button"
428
+ onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
429
+ className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
430
+ >
431
+ + New
432
+ </button>
433
+ </div>
434
+ ) : (
435
+ <div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
436
+ <input
437
+ type="text"
438
+ value={newKeyName}
439
+ onChange={(e) => setNewKeyName(e.target.value)}
440
+ placeholder="Key name (optional)"
441
+ className={inputClass}
442
+ style={{ fontFamily: 'inherit' }}
443
+ />
444
+ <input
445
+ type="password"
446
+ value={newKeyValue}
447
+ onChange={(e) => setNewKeyValue(e.target.value)}
448
+ placeholder="Paste API key..."
449
+ className={inputClass}
450
+ style={{ fontFamily: 'inherit' }}
451
+ />
452
+ <div className="flex gap-2 justify-end">
453
+ {credList.length > 0 && (
454
+ <button type="button" onClick={() => setAddingKey(false)} className="px-3 py-1.5 text-[12px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>Cancel</button>
455
+ )}
456
+ <button
457
+ type="button"
458
+ disabled={savingKey || !newKeyValue.trim()}
459
+ onClick={async () => {
460
+ setSavingKey(true)
461
+ try {
462
+ const cred = await api<any>('POST', '/credentials', { provider: editingId || name || 'custom', name: newKeyName.trim() || `${name || editingId || 'Custom'} key`, apiKey: newKeyValue.trim() })
463
+ await loadCredentials()
464
+ setCredentialId(cred.id)
465
+ setAddingKey(false)
466
+ setNewKeyName('')
467
+ setNewKeyValue('')
468
+ } catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
469
+ finally { setSavingKey(false) }
470
+ }}
471
+ className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40"
472
+ style={{ fontFamily: 'inherit' }}
473
+ >
474
+ {savingKey ? 'Saving...' : 'Save Key'}
475
+ </button>
476
+ </div>
477
+ </div>
478
+ )}
479
+ </div>
480
+ )}
481
+
482
+ {/* Enabled toggle — only for custom */}
483
+ {!isBuiltin && editingCustom && (
484
+ <div className="mb-8">
485
+ <label className="flex items-center gap-3 cursor-pointer">
486
+ <div
487
+ onClick={() => setIsEnabled(!isEnabled)}
488
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
489
+ ${isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
490
+ >
491
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
492
+ ${isEnabled ? 'left-[22px]' : 'left-0.5'}`} />
493
+ </div>
494
+ <span className="font-display text-[14px] font-600 text-text-2">Enabled</span>
495
+ </label>
496
+ </div>
497
+ )}
498
+
499
+ {/* Test connection result */}
500
+ {testStatus === 'fail' && (
501
+ <div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
502
+ <p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
503
+ </div>
504
+ )}
505
+ {testStatus === 'pass' && (
506
+ <div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
507
+ <p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
508
+ </div>
509
+ )}
510
+
511
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
512
+ {editingCustom && (
513
+ <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
514
+ Delete
515
+ </button>
516
+ )}
517
+ <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
518
+ Cancel
519
+ </button>
520
+ {showApiKey && credentialId && testStatus !== 'pass' ? (
521
+ <button
522
+ onClick={handleTestConnection}
523
+ disabled={testStatus === 'testing'}
524
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-emerald-600 text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(16,185,129,0.2)] hover:brightness-110"
525
+ style={{ fontFamily: 'inherit' }}
526
+ >
527
+ {testStatus === 'testing' ? 'Testing...' : testStatus === 'fail' ? 'Retry Connection' : 'Test Connection'}
528
+ </button>
529
+ ) : (
530
+ <button
531
+ onClick={handleSave}
532
+ disabled={isBuiltin ? false : (!name.trim() || !baseUrl.trim())}
533
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
534
+ style={{ fontFamily: 'inherit' }}
535
+ >
536
+ {editing ? 'Save' : 'Create'}
537
+ </button>
538
+ )}
539
+ </div>
540
+ </BottomSheet>
541
+ )
542
+ }