@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,1220 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
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
+ import type { ProviderType, ClaudeSkill } from '@/types'
11
+
12
+ const AVAILABLE_TOOLS: { id: string; label: string; description: string }[] = [
13
+ { id: 'shell', label: 'Shell', description: 'Execute commands in the working directory' },
14
+ { id: 'files', label: 'Files', description: 'Read, write, list, move, copy, and send files' },
15
+ { id: 'copy_file', label: 'Copy File', description: 'Copy files within the working directory' },
16
+ { id: 'move_file', label: 'Move File', description: 'Move/rename files within the working directory' },
17
+ { id: 'delete_file', label: 'Delete File', description: 'Delete files/directories (disabled by default)' },
18
+ { id: 'edit_file', label: 'Edit File', description: 'Search-and-replace editing within files' },
19
+ { id: 'process', label: 'Process', description: 'Monitor and control long-running shell commands' },
20
+ { id: 'web_search', label: 'Web Search', description: 'Search the web via DuckDuckGo' },
21
+ { id: 'web_fetch', label: 'Web Fetch', description: 'Fetch and extract text from URLs' },
22
+ { id: 'claude_code', label: 'Claude Code', description: 'Delegate complex tasks to Claude Code CLI' },
23
+ { id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
24
+ { id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
25
+ { id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
26
+ { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across sessions' },
27
+ ]
28
+
29
+ const PLATFORM_TOOLS: { id: string; label: string; description: string }[] = [
30
+ { id: 'manage_agents', label: 'Agents', description: 'Create, edit, and delete agents' },
31
+ { id: 'manage_tasks', label: 'Tasks', description: 'Create, edit, and delete tasks' },
32
+ { id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
33
+ { id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
34
+ { id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
35
+ { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent sessions' },
36
+ { id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
37
+ { id: 'manage_sessions', label: 'Sessions', description: 'List sessions, send messages, and spawn session work' },
38
+ { id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
39
+ ]
40
+
41
+ const NATIVE_CAPABILITY_PROVIDER_IDS = new Set<ProviderType>(['claude-cli', 'codex-cli', 'opencode-cli', 'openclaw'])
42
+
43
+ export function AgentSheet() {
44
+ const open = useAppStore((s) => s.agentSheetOpen)
45
+ const setOpen = useAppStore((s) => s.setAgentSheetOpen)
46
+ const editingId = useAppStore((s) => s.editingAgentId)
47
+ const setEditingId = useAppStore((s) => s.setEditingAgentId)
48
+ const agents = useAppStore((s) => s.agents)
49
+ const loadAgents = useAppStore((s) => s.loadAgents)
50
+ const providers = useAppStore((s) => s.providers)
51
+ const loadProviders = useAppStore((s) => s.loadProviders)
52
+ const credentials = useAppStore((s) => s.credentials)
53
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
54
+ const dynamicSkills = useAppStore((s) => s.skills)
55
+ const mcpServers = useAppStore((s) => s.mcpServers)
56
+ const loadSkills = useAppStore((s) => s.loadSkills)
57
+ const appSettings = useAppStore((s) => s.appSettings)
58
+ const loadSettings = useAppStore((s) => s.loadSettings)
59
+
60
+ const [claudeSkills, setClaudeSkills] = useState<ClaudeSkill[]>([])
61
+ const [claudeSkillsLoading, setClaudeSkillsLoading] = useState(false)
62
+ const loadClaudeSkills = async () => {
63
+ setClaudeSkillsLoading(true)
64
+ try {
65
+ const skills = await api<ClaudeSkill[]>('GET', '/claude-skills')
66
+ setClaudeSkills(skills)
67
+ } catch { /* ignore */ }
68
+ finally { setClaudeSkillsLoading(false) }
69
+ }
70
+
71
+ const [name, setName] = useState('')
72
+ const [description, setDescription] = useState('')
73
+ const [soul, setSoul] = useState('')
74
+ const [systemPrompt, setSystemPrompt] = useState('')
75
+ const [provider, setProvider] = useState<ProviderType>('claude-cli')
76
+ const [model, setModel] = useState('')
77
+ const [credentialId, setCredentialId] = useState<string | null>(null)
78
+ const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
79
+ const [isOrchestrator, setIsOrchestrator] = useState(false)
80
+ const [subAgentIds, setAgentAgentIds] = useState<string[]>([])
81
+ const [tools, setTools] = useState<string[]>([])
82
+ const [skills, setSkills] = useState<string[]>([])
83
+ const [skillIds, setSkillIds] = useState<string[]>([])
84
+ const [mcpServerIds, setMcpServerIds] = useState<string[]>([])
85
+ const [mcpDisabledTools, setMcpDisabledTools] = useState<string[]>([])
86
+ const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
87
+ const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
88
+ const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
89
+ const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
90
+ const [capabilities, setCapabilities] = useState<string[]>([])
91
+ const [capInput, setCapInput] = useState('')
92
+ const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
93
+ const [openclawEnabled, setOpenclawEnabled] = useState(false)
94
+ const [addingKey, setAddingKey] = useState(false)
95
+ const [newKeyName, setNewKeyName] = useState('')
96
+ const [newKeyValue, setNewKeyValue] = useState('')
97
+ const [savingKey, setSavingKey] = useState(false)
98
+
99
+ // Test connection state
100
+ const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
101
+ const [testMessage, setTestMessage] = useState('')
102
+ const [testErrorCode, setTestErrorCode] = useState<string | null>(null)
103
+ const [testDeviceId, setTestDeviceId] = useState<string | null>(null)
104
+
105
+ const soulFileRef = useRef<HTMLInputElement>(null)
106
+ const promptFileRef = useRef<HTMLInputElement>(null)
107
+ const importFileRef = useRef<HTMLInputElement>(null)
108
+
109
+ const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
110
+ const file = e.target.files?.[0]
111
+ if (!file) return
112
+ const reader = new FileReader()
113
+ reader.onload = (ev) => setter(ev.target?.result as string)
114
+ reader.readAsText(file)
115
+ e.target.value = ''
116
+ }
117
+
118
+ // AI generation state
119
+ const [aiPrompt, setAiPrompt] = useState('')
120
+ const [generating, setGenerating] = useState(false)
121
+ const [generated, setGenerated] = useState(false)
122
+ const [genError, setGenError] = useState('')
123
+
124
+ const currentProvider = providers.find((p) => p.id === provider)
125
+ const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
126
+ const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
127
+ const editing = editingId ? agents[editingId] : null
128
+ const hasNativeCapabilities = NATIVE_CAPABILITY_PROVIDER_IDS.has(provider)
129
+
130
+ const providerNeedsKey = !editing && (
131
+ (currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
132
+ (provider === 'ollama' && ollamaMode === 'cloud' && providerCredentials.length === 0 && !addingKey)
133
+ )
134
+
135
+ useEffect(() => {
136
+ if (open) {
137
+ loadProviders()
138
+ loadCredentials()
139
+ loadSkills()
140
+ loadClaudeSkills()
141
+ loadSettings()
142
+ setAiPrompt('')
143
+ setGenerating(false)
144
+ setGenerated(false)
145
+ setGenError('')
146
+ setTestStatus('idle')
147
+ setTestMessage('')
148
+ if (editing) {
149
+ setName(editing.name)
150
+ setDescription(editing.description)
151
+ setSoul(editing.soul || '')
152
+ setSystemPrompt(editing.systemPrompt)
153
+ setProvider(editing.provider)
154
+ setModel(editing.model)
155
+ setCredentialId(editing.credentialId || null)
156
+ setApiEndpoint(editing.apiEndpoint || null)
157
+ setIsOrchestrator(editing.isOrchestrator || false)
158
+ setAgentAgentIds(editing.subAgentIds || [])
159
+ setTools(editing.tools || [])
160
+ setSkills(editing.skills || [])
161
+ setSkillIds(editing.skillIds || [])
162
+ setMcpServerIds(editing.mcpServerIds || [])
163
+ setMcpDisabledTools(editing.mcpDisabledTools || [])
164
+ setFallbackCredentialIds(editing.fallbackCredentialIds || [])
165
+ setPlatformAssignScope(editing.platformAssignScope || 'self')
166
+ setCapabilities(editing.capabilities || [])
167
+ setCapInput('')
168
+ setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
169
+ setOpenclawEnabled(editing.provider === 'openclaw')
170
+ } else {
171
+ setName('')
172
+ setDescription('')
173
+ setSoul('')
174
+ setSystemPrompt('')
175
+ setProvider('claude-cli')
176
+ setModel('')
177
+ setCredentialId(null)
178
+ setApiEndpoint(null)
179
+ setIsOrchestrator(false)
180
+ setAgentAgentIds([])
181
+ setTools([])
182
+ setSkills([])
183
+ setSkillIds([])
184
+ setMcpDisabledTools([])
185
+ setFallbackCredentialIds([])
186
+ setPlatformAssignScope('self')
187
+ setCapabilities([])
188
+ setCapInput('')
189
+ setOllamaMode('local')
190
+ setOpenclawEnabled(false)
191
+ }
192
+ }
193
+ // eslint-disable-next-line react-hooks/exhaustive-deps
194
+ }, [open, editingId])
195
+
196
+ useEffect(() => {
197
+ if (currentProvider?.models.length && !editing) {
198
+ setModel(currentProvider.models[0])
199
+ }
200
+ // eslint-disable-next-line react-hooks/exhaustive-deps
201
+ }, [provider, providers])
202
+
203
+ // Reset test status when connection params change
204
+ useEffect(() => {
205
+ setTestStatus('idle')
206
+ setTestMessage('')
207
+ }, [provider, credentialId, apiEndpoint])
208
+
209
+ // Fetch MCP tools when selected servers change
210
+ useEffect(() => {
211
+ if (!mcpServerIds.length) {
212
+ setMcpTools({})
213
+ return
214
+ }
215
+ let cancelled = false
216
+ setMcpToolsLoading(true)
217
+ Promise.all(
218
+ mcpServerIds.map(async (id) => {
219
+ try {
220
+ const tools = await api<{ name: string; description: string }[]>('GET', `/mcp-servers/${id}/tools`)
221
+ return { id, tools: Array.isArray(tools) ? tools : [] }
222
+ } catch {
223
+ return { id, tools: [] }
224
+ }
225
+ })
226
+ ).then((results) => {
227
+ if (cancelled) return
228
+ const map: Record<string, { name: string; description: string }[]> = {}
229
+ for (const r of results) map[r.id] = r.tools
230
+ setMcpTools(map)
231
+ setMcpToolsLoading(false)
232
+ })
233
+ return () => { cancelled = true }
234
+ // eslint-disable-next-line react-hooks/exhaustive-deps
235
+ }, [mcpServerIds.join(',')])
236
+
237
+ const handleGenerate = async () => {
238
+ if (!aiPrompt.trim()) return
239
+ setGenerating(true)
240
+ setGenError('')
241
+ try {
242
+ const result = await api<{ name?: string; description?: string; systemPrompt?: string; isOrchestrator?: boolean; error?: string }>('POST', '/agents/generate', { prompt: aiPrompt })
243
+ if (result.error) {
244
+ setGenError(result.error)
245
+ } else if (result.name || result.systemPrompt) {
246
+ if (result.name) setName(result.name)
247
+ if (result.description) setDescription(result.description)
248
+ if (result.systemPrompt) setSystemPrompt(result.systemPrompt)
249
+ if (result.isOrchestrator !== undefined) setIsOrchestrator(result.isOrchestrator)
250
+ setGenerated(true)
251
+ } else {
252
+ setGenError('AI returned empty response — try again')
253
+ }
254
+ } catch (err: unknown) {
255
+ setGenError(err instanceof Error ? err.message : 'Generation failed')
256
+ }
257
+ setGenerating(false)
258
+ }
259
+
260
+ const onClose = () => {
261
+ setOpen(false)
262
+ setEditingId(null)
263
+ }
264
+
265
+ const handleSave = async () => {
266
+ // For any endpoint, just ensure bare host:port gets a protocol prepended
267
+ let normalizedEndpoint = apiEndpoint
268
+ if (normalizedEndpoint) {
269
+ const url = normalizedEndpoint.trim().replace(/\/+$/, '')
270
+ normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
271
+ }
272
+ const data = {
273
+ name: name.trim() || 'Unnamed Agent',
274
+ description,
275
+ soul,
276
+ systemPrompt,
277
+ provider,
278
+ model,
279
+ credentialId,
280
+ apiEndpoint: normalizedEndpoint,
281
+ isOrchestrator,
282
+ subAgentIds: isOrchestrator ? subAgentIds : [],
283
+ tools,
284
+ skills,
285
+ skillIds,
286
+ mcpServerIds,
287
+ mcpDisabledTools: mcpDisabledTools.length ? mcpDisabledTools : undefined,
288
+ fallbackCredentialIds,
289
+ platformAssignScope,
290
+ capabilities,
291
+ }
292
+ if (editing) {
293
+ await updateAgent(editing.id, data)
294
+ } else {
295
+ await createAgent(data)
296
+ }
297
+ await loadAgents()
298
+ onClose()
299
+ }
300
+
301
+ const handleDelete = async () => {
302
+ if (editing) {
303
+ await deleteAgent(editing.id)
304
+ await loadAgents()
305
+ onClose()
306
+ }
307
+ }
308
+
309
+ const handleExport = () => {
310
+ if (!editing) return
311
+ const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...exportData } = editing
312
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
313
+ const url = URL.createObjectURL(blob)
314
+ const a = document.createElement('a')
315
+ a.href = url
316
+ a.download = `${editing.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.agent.json`
317
+ a.click()
318
+ URL.revokeObjectURL(url)
319
+ toast.success('Agent exported')
320
+ }
321
+
322
+ const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
323
+ const file = e.target.files?.[0]
324
+ if (!file) return
325
+ const reader = new FileReader()
326
+ reader.onload = async (ev) => {
327
+ try {
328
+ const data = JSON.parse(ev.target?.result as string)
329
+ // Strip IDs and timestamps
330
+ const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = data
331
+ await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
332
+ await loadAgents()
333
+ toast.success('Agent imported')
334
+ onClose()
335
+ } catch (err) {
336
+ toast.error('Invalid agent JSON file')
337
+ }
338
+ }
339
+ reader.readAsText(file)
340
+ e.target.value = ''
341
+ }
342
+
343
+ const handleTestConnection = async (): Promise<boolean> => {
344
+ setTestStatus('testing')
345
+ setTestMessage('')
346
+ setTestErrorCode(null)
347
+ try {
348
+ const result = await api<{ ok: boolean; message: string; errorCode?: string; deviceId?: string }>('POST', '/setup/check-provider', {
349
+ provider,
350
+ credentialId,
351
+ endpoint: apiEndpoint,
352
+ model,
353
+ })
354
+ if (result.deviceId) setTestDeviceId(result.deviceId)
355
+ if (result.ok) {
356
+ setTestStatus('pass')
357
+ setTestMessage(result.message)
358
+ return true
359
+ } else {
360
+ setTestStatus('fail')
361
+ setTestMessage(result.message)
362
+ setTestErrorCode(result.errorCode || null)
363
+ return false
364
+ }
365
+ } catch (err: unknown) {
366
+ setTestStatus('fail')
367
+ setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
368
+ return false
369
+ }
370
+ }
371
+
372
+ // Whether this provider needs a connection test before saving.
373
+ // Only CLI providers (no remote connection) skip the test.
374
+ const CLI_ONLY_PROVIDERS: Set<ProviderType> = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
375
+ const needsTest = !providerNeedsKey && !CLI_ONLY_PROVIDERS.has(provider)
376
+
377
+ const [saving, setSaving] = useState(false)
378
+
379
+ const handleTestAndSave = async () => {
380
+ if (needsTest) {
381
+ const passed = await handleTestConnection()
382
+ if (!passed) return
383
+ // Brief pause so the user can see the success state on the button
384
+ await new Promise((r) => setTimeout(r, 1500))
385
+ }
386
+ setSaving(true)
387
+ await handleSave()
388
+ setSaving(false)
389
+ }
390
+
391
+ const agentOptions = Object.values(agents).filter((p) => !p.isOrchestrator && p.id !== editingId)
392
+
393
+ const toggleAgent = (id: string) => {
394
+ setAgentAgentIds((prev) =>
395
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
396
+ )
397
+ }
398
+
399
+ 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"
400
+
401
+ return (
402
+ <BottomSheet open={open} onClose={onClose} wide>
403
+ <div className="mb-10">
404
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
405
+ {editing ? 'Edit Agent' : 'New Agent'}
406
+ </h2>
407
+ <p className="text-[14px] text-text-3">Define an AI agent or orchestrator</p>
408
+ </div>
409
+
410
+ {/* AI Generation */}
411
+ {!editing && <AiGenBlock
412
+ aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
413
+ generating={generating} generated={generated} genError={genError}
414
+ onGenerate={handleGenerate} appSettings={appSettings}
415
+ placeholder='Describe the agent you want, e.g. "An SEO keyword researcher that finds low-competition long-tail keywords"'
416
+ />}
417
+
418
+ <div className="mb-8">
419
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
420
+ <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
421
+ </div>
422
+
423
+ <div className="mb-8">
424
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
425
+ <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
426
+ </div>
427
+
428
+ {/* Capabilities */}
429
+ <div className="mb-8">
430
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
431
+ Capabilities <span className="normal-case tracking-normal font-normal text-text-3">(for agent delegation)</span>
432
+ </label>
433
+ <div className="flex flex-wrap gap-1.5 mb-2">
434
+ {capabilities.map((cap) => (
435
+ <span
436
+ key={cap}
437
+ className="inline-flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-accent-soft text-accent-bright text-[12px] font-600"
438
+ >
439
+ {cap}
440
+ <button
441
+ onClick={() => setCapabilities((prev) => prev.filter((c) => c !== cap))}
442
+ className="bg-transparent border-none text-accent-bright/60 hover:text-accent-bright cursor-pointer text-[14px] leading-none p-0"
443
+ >
444
+ x
445
+ </button>
446
+ </span>
447
+ ))}
448
+ </div>
449
+ <div className="flex gap-2">
450
+ <input
451
+ type="text"
452
+ value={capInput}
453
+ onChange={(e) => setCapInput(e.target.value)}
454
+ onKeyDown={(e) => {
455
+ if ((e.key === 'Enter' || e.key === ',') && capInput.trim()) {
456
+ e.preventDefault()
457
+ const val = capInput.trim().toLowerCase().replace(/,/g, '')
458
+ if (val && !capabilities.includes(val)) {
459
+ setCapabilities((prev) => [...prev, val])
460
+ }
461
+ setCapInput('')
462
+ }
463
+ }}
464
+ placeholder="e.g. frontend, research, devops"
465
+ className={inputClass}
466
+ style={{ fontFamily: 'inherit' }}
467
+ />
468
+ </div>
469
+ <p className="text-[11px] text-text-3/70 mt-1.5">Press Enter or comma to add. Other agents see these when deciding delegation.</p>
470
+ </div>
471
+
472
+ {provider !== 'openclaw' && (
473
+ <div className="mb-8">
474
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
475
+ Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
476
+ </label>
477
+ <div className="flex items-center gap-2 mb-3">
478
+ <p className="text-[12px] text-text-3/60">Define the agent&apos;s voice, tone, and personality. Injected before the system prompt.</p>
479
+ <button onClick={() => soulFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
480
+ <input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
481
+ </div>
482
+ <textarea
483
+ value={soul}
484
+ onChange={(e) => setSoul(e.target.value)}
485
+ placeholder="e.g. You speak concisely and directly. You have a dry sense of humor. You always back claims with data."
486
+ rows={3}
487
+ className={`${inputClass} resize-y min-h-[80px]`}
488
+ style={{ fontFamily: 'inherit' }}
489
+ />
490
+ </div>
491
+ )}
492
+
493
+ {provider !== 'openclaw' && (
494
+ <div className="mb-8">
495
+ <div className="flex items-center gap-2 mb-3">
496
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt</label>
497
+ <button onClick={() => promptFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
498
+ <input ref={promptFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSystemPrompt)} className="hidden" />
499
+ </div>
500
+ <textarea
501
+ value={systemPrompt}
502
+ onChange={(e) => setSystemPrompt(e.target.value)}
503
+ placeholder="You are an expert..."
504
+ rows={5}
505
+ className={`${inputClass} resize-y min-h-[120px]`}
506
+ style={{ fontFamily: 'inherit' }}
507
+ />
508
+ </div>
509
+ )}
510
+
511
+ {/* OpenClaw Gateway Toggle */}
512
+ <div className="mb-8">
513
+ <div className="flex items-center justify-between">
514
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">OpenClaw Gateway</label>
515
+ <button
516
+ type="button"
517
+ onClick={() => {
518
+ if (!openclawEnabled) {
519
+ setOpenclawEnabled(true)
520
+ setProvider('openclaw')
521
+ setModel('default')
522
+ if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
523
+ } else {
524
+ setOpenclawEnabled(false)
525
+ setProvider('claude-cli')
526
+ setModel('')
527
+ setApiEndpoint(null)
528
+ setCredentialId(null)
529
+ }
530
+ }}
531
+ className={`relative w-11 h-6 rounded-full transition-colors duration-200 cursor-pointer border-none ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
532
+ >
533
+ <span className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ${openclawEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
534
+ </button>
535
+ </div>
536
+ {openclawEnabled && (
537
+ <div className="mt-4 space-y-4">
538
+ <div>
539
+ <label className="block text-[12px] text-text-3 mb-2">Gateway URL</label>
540
+ <input
541
+ type="text"
542
+ value={apiEndpoint || ''}
543
+ onChange={(e) => setApiEndpoint(e.target.value || null)}
544
+ placeholder="http://localhost:18789"
545
+ className={inputClass}
546
+ style={{ fontFamily: 'inherit' }}
547
+ />
548
+ </div>
549
+ <div>
550
+ <label className="block text-[12px] text-text-3 mb-2">Gateway Token</label>
551
+ {openclawCredentials.length > 0 && !addingKey ? (
552
+ <div className="flex gap-2">
553
+ <select value={credentialId || ''} onChange={(e) => {
554
+ if (e.target.value === '__add__') {
555
+ setAddingKey(true)
556
+ setNewKeyName('')
557
+ setNewKeyValue('')
558
+ } else {
559
+ setCredentialId(e.target.value || null)
560
+ }
561
+ }} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
562
+ <option value="">Select a token...</option>
563
+ {openclawCredentials.map((c) => (
564
+ <option key={c.id} value={c.id}>{c.name}</option>
565
+ ))}
566
+ <option value="__add__">+ Add new token...</option>
567
+ </select>
568
+ <button
569
+ type="button"
570
+ onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
571
+ 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"
572
+ >
573
+ + New
574
+ </button>
575
+ </div>
576
+ ) : (
577
+ <div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
578
+ <input
579
+ type="text"
580
+ value={newKeyName}
581
+ onChange={(e) => setNewKeyName(e.target.value)}
582
+ placeholder="Token name (optional)"
583
+ className={inputClass}
584
+ style={{ fontFamily: 'inherit' }}
585
+ />
586
+ <input
587
+ type="password"
588
+ value={newKeyValue}
589
+ onChange={(e) => setNewKeyValue(e.target.value)}
590
+ placeholder="Paste gateway token..."
591
+ className={inputClass}
592
+ style={{ fontFamily: 'inherit' }}
593
+ />
594
+ <div className="flex gap-2 justify-end">
595
+ {openclawCredentials.length > 0 && (
596
+ <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>
597
+ )}
598
+ <button
599
+ type="button"
600
+ disabled={savingKey || !newKeyValue.trim()}
601
+ onClick={async () => {
602
+ setSavingKey(true)
603
+ try {
604
+ const cred = await api<any>('POST', '/credentials', { provider: 'openclaw', name: newKeyName.trim() || 'OpenClaw token', apiKey: newKeyValue.trim() })
605
+ await loadCredentials()
606
+ setCredentialId(cred.id)
607
+ setAddingKey(false)
608
+ setNewKeyName('')
609
+ setNewKeyValue('')
610
+ } catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
611
+ finally { setSavingKey(false) }
612
+ }}
613
+ 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"
614
+ style={{ fontFamily: 'inherit' }}
615
+ >
616
+ {savingKey ? 'Saving...' : 'Save Token'}
617
+ </button>
618
+ </div>
619
+ </div>
620
+ )}
621
+ </div>
622
+ <p className="text-[11px] text-text-3/70">Enter the URL and token for your local or remote OpenClaw gateway.</p>
623
+ {/* Insecure connection warning */}
624
+ {(() => {
625
+ const url = (apiEndpoint || '').trim().toLowerCase()
626
+ const isRemote = url && !/localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url)
627
+ const isSecure = /^(https|wss):\/\//i.test(url)
628
+ if (isRemote && !isSecure) return (
629
+ <div className="mt-3 p-3 rounded-[10px] bg-[#fbbf24]/10 border border-[#fbbf24]/30">
630
+ <p className="text-[11px] text-[#fbbf24] font-500 leading-[1.5]">
631
+ This connection is not encrypted. Credentials and chat data could be intercepted on the network.
632
+ For production use, put your gateway behind HTTPS (e.g. Caddy, nginx) or use an SSH tunnel.
633
+ </p>
634
+ </div>
635
+ )
636
+ return null
637
+ })()}
638
+ {/* OpenClaw troubleshooting — shown on any test failure */}
639
+ {testStatus === 'fail' && openclawEnabled && (
640
+ <div className="mt-3 p-4 rounded-[12px] bg-accent-soft/30 border border-accent-bright/20">
641
+ {testErrorCode === 'PAIRING_REQUIRED' ? (
642
+ <div className="space-y-2">
643
+ <p className="text-[12px] text-accent-bright font-600 mb-1">Device Pairing Required</p>
644
+ <p className="text-[11px] text-text-3 leading-[1.6]">
645
+ Your gateway needs to approve this SwarmClaw instance before it can connect. Follow these steps:
646
+ </p>
647
+ <ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
648
+ <li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
649
+ <li>Go to <span className="text-text-2 font-500">Devices</span></li>
650
+ <li>Find and approve the pending device {testDeviceId ? <span className="text-text-2 font-mono text-[10px]">({testDeviceId.slice(0, 12)}...)</span> : null}</li>
651
+ <li>Come back here and click <span className="text-text-2 font-500">Test &amp; Save</span> again</li>
652
+ </ol>
653
+ </div>
654
+ ) : testErrorCode === 'DEVICE_AUTH_INVALID' ? (
655
+ <div className="space-y-2">
656
+ <p className="text-[12px] text-accent-bright font-600 mb-1">Device Authentication Failed</p>
657
+ <p className="text-[11px] text-text-3 leading-[1.6]">
658
+ The gateway rejected this device&apos;s signature. This usually means it needs to be paired first, or there&apos;s a protocol mismatch.
659
+ </p>
660
+ <p className="text-[11px] text-text-3 font-500 mt-2 mb-1">Try these steps:</p>
661
+ <ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
662
+ <li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
663
+ <li>Go to <span className="text-text-2 font-500">Devices</span> and look for a pending device request</li>
664
+ <li>If the device is listed, approve it and click <span className="text-text-2 font-500">Test &amp; Save</span> again</li>
665
+ <li>If not listed, update your gateway to the latest version and restart it</li>
666
+ </ol>
667
+ </div>
668
+ ) : (
669
+ <div className="space-y-2">
670
+ <p className="text-[12px] text-accent-bright font-600 mb-1">Connection Failed</p>
671
+ <p className="text-[11px] text-text-3 leading-[1.6]">
672
+ Could not connect to the OpenClaw gateway. Check the following:
673
+ </p>
674
+ <ul className="text-[11px] text-text-3 leading-[1.8] list-disc list-inside space-y-1">
675
+ <li>The gateway is running and reachable at the URL above</li>
676
+ <li>The gateway token matches exactly (if required)</li>
677
+ <li>No firewall is blocking the connection</li>
678
+ </ul>
679
+ </div>
680
+ )}
681
+ {testDeviceId && (
682
+ <div className="mt-3 pt-3 border-t border-white/[0.06]">
683
+ <p className="text-[10px] text-text-3/60">
684
+ SwarmClaw Device ID: <span className="font-mono text-text-3/80 select-all">{testDeviceId}</span>
685
+ </p>
686
+ </div>
687
+ )}
688
+ </div>
689
+ )}
690
+ {/* Device ID info — shown after any successful OpenClaw test */}
691
+ {testStatus === 'pass' && openclawEnabled && testDeviceId && (
692
+ <div className="mt-3 px-3 py-2 rounded-[10px] bg-emerald-500/[0.06] border border-emerald-500/15">
693
+ <p className="text-[10px] text-text-3/60">
694
+ Device ID: <span className="font-mono text-emerald-400/70 select-all">{testDeviceId.slice(0, 16)}...</span>
695
+ </p>
696
+ </div>
697
+ )}
698
+ </div>
699
+ )}
700
+ </div>
701
+
702
+ {!openclawEnabled && <div className="mb-8">
703
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Provider</label>
704
+ <div className="grid grid-cols-3 gap-3">
705
+ {providers.filter((p) => !isOrchestrator || p.id !== 'claude-cli').map((p) => {
706
+ const isConnected = !p.requiresApiKey || Object.values(credentials).some((c) => c.provider === p.id)
707
+ return (
708
+ <button
709
+ key={p.id}
710
+ onClick={() => {
711
+ setProvider(p.id)
712
+ }}
713
+ className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
714
+ active:scale-[0.97] text-[14px] font-600 border
715
+ ${provider === p.id
716
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
717
+ : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
718
+ style={{ fontFamily: 'inherit' }}
719
+ >
720
+ {isConnected && (
721
+ <span className="absolute top-2 right-2 w-2 h-2 rounded-full bg-emerald-400" />
722
+ )}
723
+ {p.name}
724
+ </button>
725
+ )
726
+ })}
727
+ </div>
728
+ </div>}
729
+
730
+ {!openclawEnabled && currentProvider && currentProvider.models.length > 0 && (
731
+ <div className="mb-8">
732
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Model</label>
733
+ <select value={model} onChange={(e) => setModel(e.target.value)} className={`${inputClass} appearance-none cursor-pointer`} style={{ fontFamily: 'inherit' }}>
734
+ {currentProvider.models.map((m) => (
735
+ <option key={m} value={m}>{m}</option>
736
+ ))}
737
+ </select>
738
+ </div>
739
+ )}
740
+
741
+ {/* Ollama Mode Toggle */}
742
+ {!openclawEnabled && provider === 'ollama' && (
743
+ <div className="mb-8">
744
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Mode</label>
745
+ <div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
746
+ {(['local', 'cloud'] as const).map((mode) => (
747
+ <button
748
+ key={mode}
749
+ onClick={() => {
750
+ setOllamaMode(mode)
751
+ if (mode === 'local') {
752
+ setApiEndpoint('http://localhost:11434')
753
+ setCredentialId(null)
754
+ } else {
755
+ setApiEndpoint(null)
756
+ if (providerCredentials.length > 0) setCredentialId(providerCredentials[0].id)
757
+ }
758
+ }}
759
+ className={`flex-1 py-3 rounded-[12px] text-center cursor-pointer transition-all duration-200
760
+ text-[14px] font-600 capitalize
761
+ ${ollamaMode === mode
762
+ ? 'bg-accent-soft text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
763
+ : 'bg-transparent text-text-3 hover:text-text-2'}`}
764
+ style={{ fontFamily: 'inherit' }}
765
+ >
766
+ {mode}
767
+ </button>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ )}
772
+
773
+ {!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && (
774
+ <div className="mb-8">
775
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
776
+ API Key{currentProvider?.optionalApiKey && !currentProvider?.requiresApiKey && <span className="normal-case tracking-normal font-normal text-text-3"> (optional)</span>}
777
+ </label>
778
+ {providerCredentials.length > 0 && !addingKey ? (
779
+ <div className="flex gap-2">
780
+ <select value={credentialId || ''} onChange={(e) => {
781
+ if (e.target.value === '__add__') {
782
+ setAddingKey(true)
783
+ setNewKeyName('')
784
+ setNewKeyValue('')
785
+ } else {
786
+ setCredentialId(e.target.value || null)
787
+ }
788
+ }} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
789
+ <option value="">Select a key...</option>
790
+ {providerCredentials.map((c) => (
791
+ <option key={c.id} value={c.id}>{c.name}</option>
792
+ ))}
793
+ <option value="__add__">+ Add new key...</option>
794
+ </select>
795
+ <button
796
+ type="button"
797
+ onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
798
+ 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"
799
+ >
800
+ + New
801
+ </button>
802
+ </div>
803
+ ) : (
804
+ <div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
805
+ <input
806
+ type="text"
807
+ value={newKeyName}
808
+ onChange={(e) => setNewKeyName(e.target.value)}
809
+ placeholder="Key name (optional)"
810
+ className={inputClass}
811
+ style={{ fontFamily: 'inherit' }}
812
+ />
813
+ <input
814
+ type="password"
815
+ value={newKeyValue}
816
+ onChange={(e) => setNewKeyValue(e.target.value)}
817
+ placeholder="Paste API key..."
818
+ className={inputClass}
819
+ style={{ fontFamily: 'inherit' }}
820
+ />
821
+ <div className="flex gap-2 justify-end">
822
+ {providerCredentials.length > 0 && (
823
+ <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>
824
+ )}
825
+ <button
826
+ type="button"
827
+ disabled={savingKey || !newKeyValue.trim()}
828
+ onClick={async () => {
829
+ setSavingKey(true)
830
+ try {
831
+ const cred = await api<any>('POST', '/credentials', { provider, name: newKeyName.trim() || `${provider} key`, apiKey: newKeyValue.trim() })
832
+ await loadCredentials()
833
+ setCredentialId(cred.id)
834
+ setAddingKey(false)
835
+ setNewKeyName('')
836
+ setNewKeyValue('')
837
+ } catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
838
+ finally { setSavingKey(false) }
839
+ }}
840
+ 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"
841
+ style={{ fontFamily: 'inherit' }}
842
+ >
843
+ {savingKey ? 'Saving...' : 'Save Key'}
844
+ </button>
845
+ </div>
846
+ </div>
847
+ )}
848
+ </div>
849
+ )}
850
+
851
+ {/* Fallback Credentials */}
852
+ {!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && providerCredentials.length > 1 && (
853
+ <div className="mb-8">
854
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
855
+ Fallback Keys <span className="normal-case tracking-normal font-normal text-text-3">(for auto-failover)</span>
856
+ </label>
857
+ <p className="text-[12px] text-text-3/60 mb-3">If the primary key fails (rate limit, auth error), these keys will be tried in order.</p>
858
+ <div className="flex flex-wrap gap-2">
859
+ {providerCredentials.filter((c) => c.id !== credentialId).map((c) => {
860
+ const active = fallbackCredentialIds.includes(c.id)
861
+ return (
862
+ <button
863
+ key={c.id}
864
+ onClick={() => setFallbackCredentialIds((prev) => active ? prev.filter((x) => x !== c.id) : [...prev, c.id])}
865
+ className={`px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
866
+ ${active
867
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
868
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
869
+ style={{ fontFamily: 'inherit' }}
870
+ >
871
+ {c.name}
872
+ </button>
873
+ )
874
+ })}
875
+ </div>
876
+ </div>
877
+ )}
878
+
879
+ {currentProvider?.requiresEndpoint && (provider === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
880
+ <div className="mb-8">
881
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
882
+ {provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}
883
+ </label>
884
+ <input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
885
+ {provider === 'openclaw' && (
886
+ <p className="text-[11px] text-text-3/60 mt-2">The /v1 endpoint of your remote OpenClaw instance</p>
887
+ )}
888
+ </div>
889
+ )}
890
+
891
+ {/* Tools — hidden for providers that manage capabilities outside LangGraph */}
892
+ {!hasNativeCapabilities && (
893
+ <div className="mb-8">
894
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Tools</label>
895
+ <p className="text-[12px] text-text-3/60 mb-3">Enable tools for LangGraph agent sessions.</p>
896
+ <div className="space-y-3">
897
+ {AVAILABLE_TOOLS.map((t) => (
898
+ <label key={t.id} className="flex items-center gap-3 cursor-pointer">
899
+ <div
900
+ onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
901
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
902
+ ${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
903
+ >
904
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
905
+ ${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
906
+ </div>
907
+ <span className="font-display text-[14px] font-600 text-text-2">{t.label}</span>
908
+ <span className="text-[12px] text-text-3">{t.description}</span>
909
+ </label>
910
+ ))}
911
+ </div>
912
+ </div>
913
+ )}
914
+
915
+ {/* Platform — hidden for providers that manage capabilities outside LangGraph */}
916
+ {!hasNativeCapabilities && (
917
+ <div className="mb-8">
918
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Platform</label>
919
+ <p className="text-[12px] text-text-3/60 mb-3">Allow this agent to manage platform resources directly.</p>
920
+ <div className="space-y-3">
921
+ {PLATFORM_TOOLS.map((t) => (
922
+ <label key={t.id} className="flex items-center gap-3 cursor-pointer">
923
+ <div
924
+ onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
925
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
926
+ ${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
927
+ >
928
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
929
+ ${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
930
+ </div>
931
+ <span className="font-display text-[14px] font-600 text-text-2">{t.label}</span>
932
+ <span className="text-[12px] text-text-3">{t.description}</span>
933
+ </label>
934
+ ))}
935
+ </div>
936
+ {(tools.includes('manage_tasks') || tools.includes('manage_schedules')) && (
937
+ <div className="mt-4 ml-1 pt-3 border-t border-white/[0.04]">
938
+ <label className="flex items-center gap-3 cursor-pointer">
939
+ <div
940
+ onClick={() => setPlatformAssignScope((prev) => prev === 'all' ? 'self' : 'all')}
941
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
942
+ ${platformAssignScope === 'all' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
943
+ >
944
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
945
+ ${platformAssignScope === 'all' ? 'left-[22px]' : 'left-0.5'}`} />
946
+ </div>
947
+ <span className="font-display text-[14px] font-600 text-text-2">Assign to Other Agents</span>
948
+ <span className="text-[12px] text-text-3">Allow this agent to assign tasks and schedules to other agents</span>
949
+ </label>
950
+ </div>
951
+ )}
952
+ </div>
953
+ )}
954
+
955
+ {/* Native capability provider note */}
956
+ {hasNativeCapabilities && (
957
+ <div className="mb-8 p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
958
+ <p className="text-[13px] text-text-3">
959
+ {provider === 'openclaw'
960
+ ? 'OpenClaw manages tools/platform capabilities in the remote OpenClaw instance — no local tool toggles are applied here.'
961
+ : provider === 'claude-cli'
962
+ ? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
963
+ : provider === 'codex-cli'
964
+ ? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
965
+ : 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
966
+ </p>
967
+ </div>
968
+ )}
969
+
970
+ {/* Skills — discovered from ~/.claude/skills/ */}
971
+ {provider === 'claude-cli' && (
972
+ <div className="mb-8">
973
+ <div className="flex items-center justify-between mb-2">
974
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
975
+ Skills <span className="normal-case tracking-normal font-normal text-text-3">(from ~/.claude/skills/)</span>
976
+ </label>
977
+ <button
978
+ onClick={loadClaudeSkills}
979
+ disabled={claudeSkillsLoading}
980
+ className="text-[11px] text-text-3 hover:text-accent-bright transition-colors cursor-pointer bg-transparent border-none flex items-center gap-1"
981
+ style={{ fontFamily: 'inherit' }}
982
+ title="Refresh skills from ~/.claude/skills/"
983
+ >
984
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
985
+ className={claudeSkillsLoading ? 'animate-spin' : ''}>
986
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
987
+ </svg>
988
+ Refresh
989
+ </button>
990
+ </div>
991
+ <p className="text-[12px] text-text-3/60 mb-3">When delegated to, this agent will be instructed to use these skills.</p>
992
+ {claudeSkills.length > 0 ? (
993
+ <div className="flex flex-wrap gap-2">
994
+ {claudeSkills.map((s) => {
995
+ const active = skills.includes(s.id)
996
+ return (
997
+ <button
998
+ key={s.id}
999
+ onClick={() => setSkills((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
1000
+ className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
1001
+ ${active
1002
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1003
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1004
+ style={{ fontFamily: 'inherit' }}
1005
+ title={s.description}
1006
+ >
1007
+ {s.name}
1008
+ </button>
1009
+ )
1010
+ })}
1011
+ </div>
1012
+ ) : (
1013
+ <p className="text-[12px] text-text-3/70">No skills found in ~/.claude/skills/</p>
1014
+ )}
1015
+ </div>
1016
+ )}
1017
+
1018
+ {/* Dynamic Skills from Skills Manager */}
1019
+ {Object.keys(dynamicSkills).length > 0 && (
1020
+ <div className="mb-8">
1021
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1022
+ Custom Skills <span className="normal-case tracking-normal font-normal text-text-3">(from Skills manager)</span>
1023
+ </label>
1024
+ <p className="text-[12px] text-text-3/60 mb-3">Skill content is injected into the system prompt when this agent runs.</p>
1025
+ <div className="flex flex-wrap gap-2">
1026
+ {Object.values(dynamicSkills).map((s) => {
1027
+ const active = skillIds.includes(s.id)
1028
+ return (
1029
+ <button
1030
+ key={s.id}
1031
+ onClick={() => setSkillIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
1032
+ className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
1033
+ ${active
1034
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1035
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1036
+ style={{ fontFamily: 'inherit' }}
1037
+ title={s.description || s.filename}
1038
+ >
1039
+ {s.name}
1040
+ </button>
1041
+ )
1042
+ })}
1043
+ </div>
1044
+ </div>
1045
+ )}
1046
+
1047
+ {/* MCP Servers */}
1048
+ {Object.keys(mcpServers).length > 0 && (
1049
+ <div className="mb-8">
1050
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1051
+ MCP Servers
1052
+ </label>
1053
+ <p className="text-[12px] text-text-3/60 mb-3">Connect external tool servers to this agent via MCP.</p>
1054
+ <div className="flex flex-wrap gap-2">
1055
+ {Object.values(mcpServers).map((s: any) => {
1056
+ const active = mcpServerIds.includes(s.id)
1057
+ return (
1058
+ <button
1059
+ key={s.id}
1060
+ onClick={() => setMcpServerIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
1061
+ className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
1062
+ ${active
1063
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1064
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1065
+ style={{ fontFamily: 'inherit' }}
1066
+ title={`${s.transport} — ${s.command || s.url || ''}`}
1067
+ >
1068
+ {s.name}
1069
+ </button>
1070
+ )
1071
+ })}
1072
+ </div>
1073
+ </div>
1074
+ )}
1075
+
1076
+ {/* MCP Tools — per-tool enable/disable toggles */}
1077
+ {mcpServerIds.length > 0 && Object.keys(mcpTools).length > 0 && (
1078
+ <div className="mb-8">
1079
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1080
+ MCP Tools
1081
+ </label>
1082
+ <p className="text-[12px] text-text-3/60 mb-3">
1083
+ Toggle individual tools from connected MCP servers.{mcpToolsLoading ? ' Loading…' : ''}
1084
+ </p>
1085
+ <div className="space-y-4">
1086
+ {mcpServerIds.map((serverId) => {
1087
+ const server = (mcpServers as Record<string, any>)[serverId]
1088
+ const serverTools = mcpTools[serverId]
1089
+ if (!server || !serverTools?.length) return null
1090
+ const safeName = server.name.replace(/[^a-zA-Z0-9_]/g, '_')
1091
+ return (
1092
+ <div key={serverId}>
1093
+ <p className="text-[12px] font-600 text-text-3 mb-2">{server.name}</p>
1094
+ <div className="space-y-3">
1095
+ {serverTools.map((t) => {
1096
+ const fullName = `mcp_${safeName}_${t.name}`
1097
+ const enabled = !mcpDisabledTools.includes(fullName)
1098
+ return (
1099
+ <label key={fullName} className="flex items-center gap-3 cursor-pointer">
1100
+ <div
1101
+ onClick={() => setMcpDisabledTools((prev) =>
1102
+ enabled ? [...prev, fullName] : prev.filter((x) => x !== fullName)
1103
+ )}
1104
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1105
+ ${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1106
+ >
1107
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1108
+ ${enabled ? 'left-[22px]' : 'left-0.5'}`} />
1109
+ </div>
1110
+ <span className="font-display text-[14px] font-600 text-text-2">{t.name}</span>
1111
+ <span className="text-[12px] text-text-3 truncate">{t.description}</span>
1112
+ </label>
1113
+ )
1114
+ })}
1115
+ </div>
1116
+ </div>
1117
+ )
1118
+ })}
1119
+ </div>
1120
+ </div>
1121
+ )}
1122
+
1123
+ {provider !== 'openclaw' && (
1124
+ <div className="mb-8">
1125
+ <label className="flex items-center gap-3 cursor-pointer">
1126
+ <div
1127
+ onClick={() => {
1128
+ const next = !isOrchestrator
1129
+ setIsOrchestrator(next)
1130
+ if (next && provider === 'claude-cli') setProvider('anthropic')
1131
+ }}
1132
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
1133
+ ${isOrchestrator ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1134
+ >
1135
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1136
+ ${isOrchestrator ? 'left-[22px]' : 'left-0.5'}`} />
1137
+ </div>
1138
+ <span className="font-display text-[14px] font-600 text-text-2">Orchestrator</span>
1139
+ <span className="text-[12px] text-text-3">Can delegate tasks to other agents</span>
1140
+ </label>
1141
+ </div>
1142
+ )}
1143
+
1144
+ {provider !== 'openclaw' && isOrchestrator && agentOptions.length > 0 && (
1145
+ <div className="mb-8">
1146
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Available Agents</label>
1147
+ <div className="flex flex-wrap gap-2">
1148
+ {agentOptions.map((a) => (
1149
+ <button
1150
+ key={a.id}
1151
+ onClick={() => toggleAgent(a.id)}
1152
+ className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
1153
+ ${subAgentIds.includes(a.id)
1154
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1155
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1156
+ style={{ fontFamily: 'inherit' }}
1157
+ >
1158
+ {a.name}
1159
+ </button>
1160
+ ))}
1161
+ </div>
1162
+ </div>
1163
+ )}
1164
+
1165
+ {/* Provider key warning */}
1166
+ {providerNeedsKey && (
1167
+ <div className="mb-4 p-3 rounded-[12px] bg-amber-500/[0.08] border border-amber-500/20">
1168
+ <p className="text-[13px] text-amber-400">
1169
+ Add an API key for {currentProvider?.name || provider} above before creating this agent.
1170
+ </p>
1171
+ </div>
1172
+ )}
1173
+
1174
+ {/* Test connection result */}
1175
+ {testStatus === 'fail' && (
1176
+ <div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
1177
+ <p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
1178
+ </div>
1179
+ )}
1180
+ {testStatus === 'pass' && (
1181
+ <div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
1182
+ <p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
1183
+ </div>
1184
+ )}
1185
+
1186
+ {/* Import file input (hidden) */}
1187
+ <input ref={importFileRef} type="file" accept=".json" onChange={handleImport} className="hidden" />
1188
+
1189
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
1190
+ {editing && (
1191
+ <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' }}>
1192
+ Delete
1193
+ </button>
1194
+ )}
1195
+ {editing && (
1196
+ <button onClick={handleExport} className="py-3.5 px-4 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 hover:text-text-2 transition-all" style={{ fontFamily: 'inherit' }} title="Export agent as JSON">
1197
+ Export
1198
+ </button>
1199
+ )}
1200
+ {!editing && (
1201
+ <button onClick={() => importFileRef.current?.click()} className="py-3.5 px-4 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 hover:text-text-2 transition-all" style={{ fontFamily: 'inherit' }} title="Import agent from JSON">
1202
+ Import
1203
+ </button>
1204
+ )}
1205
+ <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' }}>
1206
+ Cancel
1207
+ </button>
1208
+ <button
1209
+ onClick={handleTestAndSave}
1210
+ disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || testStatus === 'pass' || saving}
1211
+ className={`flex-1 py-3.5 rounded-[14px] border-none text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-60 transition-all hover:brightness-110
1212
+ ${testStatus === 'pass' ? 'bg-emerald-600 shadow-[0_4px_20px_rgba(16,185,129,0.25)]' : 'bg-[#6366F1] shadow-[0_4px_20px_rgba(99,102,241,0.25)]'}`}
1213
+ style={{ fontFamily: 'inherit' }}
1214
+ >
1215
+ {testStatus === 'testing' ? 'Testing...' : testStatus === 'pass' ? (saving ? 'Saving...' : 'Connected!') : needsTest ? 'Test & Save' : editing ? 'Save' : 'Create'}
1216
+ </button>
1217
+ </div>
1218
+ </BottomSheet>
1219
+ )
1220
+ }