@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,99 @@
1
+ 'use client'
2
+
3
+ import type { SettingsSectionProps } from './types'
4
+
5
+ interface EmbeddingSectionProps extends SettingsSectionProps {
6
+ credList: Array<{ id: string; name: string; provider: string }>
7
+ }
8
+
9
+ export function EmbeddingSection({ appSettings, patchSettings, inputClass, credList }: EmbeddingSectionProps) {
10
+ return (
11
+ <div className="mb-10">
12
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
13
+ Embeddings
14
+ </h3>
15
+ <p className="text-[12px] text-text-3 mb-5">
16
+ Enable semantic search for agent memory. Requires an embedding model provider.
17
+ </p>
18
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
19
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Provider</label>
20
+ <div className="grid grid-cols-4 gap-2 mb-5">
21
+ {[
22
+ { id: null, name: 'Off' },
23
+ { id: 'local' as const, name: 'Local (Free)' },
24
+ { id: 'openai' as const, name: 'OpenAI' },
25
+ { id: 'ollama' as const, name: 'Ollama' },
26
+ ].map((p) => (
27
+ <button
28
+ key={String(p.id)}
29
+ onClick={() => patchSettings({ embeddingProvider: p.id, embeddingModel: null, embeddingCredentialId: null })}
30
+ className={`py-3 px-3 rounded-[12px] text-center cursor-pointer transition-all text-[13px] font-600 border
31
+ ${(appSettings.embeddingProvider || null) === p.id
32
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
33
+ : 'bg-bg border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
34
+ style={{ fontFamily: 'inherit' }}
35
+ >
36
+ {p.name}
37
+ </button>
38
+ ))}
39
+ </div>
40
+
41
+ {appSettings.embeddingProvider === 'local' && (
42
+ <p className="text-[12px] text-text-3/80 mb-5">
43
+ Runs <span className="text-text-2 font-600">all-MiniLM-L6-v2</span> locally in Node.js — no API key, no cost, works offline. Model downloads once (~23MB).
44
+ </p>
45
+ )}
46
+
47
+ {appSettings.embeddingProvider === 'openai' && (
48
+ <>
49
+ <div className="mb-5">
50
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Model</label>
51
+ <select
52
+ value={appSettings.embeddingModel || 'text-embedding-3-small'}
53
+ onChange={(e) => patchSettings({ embeddingModel: e.target.value })}
54
+ className={`${inputClass} appearance-none cursor-pointer`}
55
+ style={{ fontFamily: 'inherit' }}
56
+ >
57
+ <option value="text-embedding-3-small">text-embedding-3-small</option>
58
+ <option value="text-embedding-3-large">text-embedding-3-large</option>
59
+ </select>
60
+ </div>
61
+ <div>
62
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
63
+ {credList.filter((c) => c.provider === 'openai').length > 0 ? (
64
+ <select
65
+ value={appSettings.embeddingCredentialId || ''}
66
+ onChange={(e) => patchSettings({ embeddingCredentialId: e.target.value || null })}
67
+ className={`${inputClass} appearance-none cursor-pointer`}
68
+ style={{ fontFamily: 'inherit' }}
69
+ >
70
+ <option value="">Select a key...</option>
71
+ {credList.filter((c) => c.provider === 'openai').map((c) => (
72
+ <option key={c.id} value={c.id}>{c.name}</option>
73
+ ))}
74
+ </select>
75
+ ) : (
76
+ <p className="text-[12px] text-text-3/60">No OpenAI API keys configured.</p>
77
+ )}
78
+ </div>
79
+ </>
80
+ )}
81
+
82
+ {appSettings.embeddingProvider === 'ollama' && (
83
+ <div>
84
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Model</label>
85
+ <input
86
+ type="text"
87
+ value={appSettings.embeddingModel || 'nomic-embed-text'}
88
+ onChange={(e) => patchSettings({ embeddingModel: e.target.value })}
89
+ placeholder="nomic-embed-text"
90
+ className={inputClass}
91
+ style={{ fontFamily: 'inherit' }}
92
+ />
93
+ <p className="text-[11px] text-text-3/60 mt-2">Uses your local Ollama instance for embeddings</p>
94
+ </div>
95
+ )}
96
+ </div>
97
+ </div>
98
+ )
99
+ }
@@ -0,0 +1,168 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
6
+ import type { SettingsSectionProps } from './types'
7
+
8
+ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
9
+ const loadSessions = useAppStore((s) => s.loadSessions)
10
+ const [disablingHeartbeats, setDisablingHeartbeats] = useState(false)
11
+ const [heartbeatBulkNotice, setHeartbeatBulkNotice] = useState('')
12
+
13
+ const handleDisableAllHeartbeats = async () => {
14
+ if (disablingHeartbeats) return
15
+ setDisablingHeartbeats(true)
16
+ setHeartbeatBulkNotice('')
17
+ try {
18
+ const result = await api<{
19
+ ok: boolean
20
+ updatedSessions: number
21
+ cancelledQueued: number
22
+ abortedRunning: number
23
+ }>('POST', '/sessions/heartbeat', { action: 'disable_all' })
24
+ await loadSessions()
25
+ setHeartbeatBulkNotice(
26
+ `Stopped heartbeat on ${result.updatedSessions} session(s); cancelled ${result.cancelledQueued} queued run(s), aborted ${result.abortedRunning} running run(s).`,
27
+ )
28
+ } catch (err: any) {
29
+ setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all sessions.')
30
+ } finally {
31
+ setDisablingHeartbeats(false)
32
+ }
33
+ }
34
+
35
+ return (
36
+ <div className="mb-10">
37
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
38
+ Heartbeat
39
+ </h3>
40
+ <p className="text-[12px] text-text-3 mb-5">
41
+ Configure ongoing heartbeat checks for long-lived sessions.
42
+ </p>
43
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
44
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
45
+ <div>
46
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Heartbeat Interval</label>
47
+ <input
48
+ type="text"
49
+ value={appSettings.heartbeatInterval ?? appSettings.heartbeatIntervalSec ?? '30m'}
50
+ onChange={(e) => {
51
+ const val = e.target.value.trim()
52
+ patchSettings({ heartbeatInterval: val || null })
53
+ }}
54
+ placeholder="30m"
55
+ className={inputClass}
56
+ style={{ fontFamily: 'inherit' }}
57
+ />
58
+ <p className="text-[11px] text-text-3/60 mt-2">Duration string (e.g. 30m, 1h) or seconds. Set to 0 to disable.</p>
59
+ </div>
60
+ <div>
61
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Heartbeat Prompt</label>
62
+ <input
63
+ type="text"
64
+ value={appSettings.heartbeatPrompt || ''}
65
+ onChange={(e) => patchSettings({ heartbeatPrompt: e.target.value || null })}
66
+ placeholder="Leave blank for default"
67
+ className={inputClass}
68
+ style={{ fontFamily: 'inherit' }}
69
+ />
70
+ </div>
71
+ </div>
72
+
73
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
74
+ <div>
75
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Heartbeat Model</label>
76
+ <input
77
+ type="text"
78
+ value={appSettings.heartbeatModel || ''}
79
+ onChange={(e) => patchSettings({ heartbeatModel: e.target.value || null })}
80
+ placeholder="Leave blank for session default"
81
+ className={inputClass}
82
+ style={{ fontFamily: 'inherit' }}
83
+ />
84
+ </div>
85
+ <div>
86
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Ack Threshold (chars)</label>
87
+ <input
88
+ type="number"
89
+ min={0}
90
+ value={appSettings.heartbeatAckMaxChars ?? 300}
91
+ onChange={(e) => {
92
+ const n = Number.parseInt(e.target.value, 10)
93
+ patchSettings({ heartbeatAckMaxChars: Number.isFinite(n) ? Math.max(0, n) : 300 })
94
+ }}
95
+ className={inputClass}
96
+ style={{ fontFamily: 'inherit' }}
97
+ />
98
+ <p className="text-[11px] text-text-3/60 mt-2">Responses under this length are suppressed as HEARTBEAT_OK.</p>
99
+ </div>
100
+ </div>
101
+
102
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
103
+ <div>
104
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Show OK Messages</label>
105
+ <button
106
+ onClick={() => patchSettings({ heartbeatShowOk: !(appSettings.heartbeatShowOk ?? false) })}
107
+ className={`px-3 py-2 rounded-[10px] border text-[12px] font-600 transition-colors cursor-pointer ${
108
+ appSettings.heartbeatShowOk
109
+ ? 'border-emerald-400/25 bg-emerald-500/10 text-emerald-300'
110
+ : 'border-white/[0.08] bg-white/[0.03] text-text-3'
111
+ }`}
112
+ style={{ fontFamily: 'inherit' }}
113
+ >
114
+ {appSettings.heartbeatShowOk ? 'On' : 'Off'}
115
+ </button>
116
+ </div>
117
+ <div>
118
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Show Alert Messages</label>
119
+ <button
120
+ onClick={() => patchSettings({ heartbeatShowAlerts: !(appSettings.heartbeatShowAlerts ?? true) })}
121
+ className={`px-3 py-2 rounded-[10px] border text-[12px] font-600 transition-colors cursor-pointer ${
122
+ (appSettings.heartbeatShowAlerts ?? true)
123
+ ? 'border-emerald-400/25 bg-emerald-500/10 text-emerald-300'
124
+ : 'border-white/[0.08] bg-white/[0.03] text-text-3'
125
+ }`}
126
+ style={{ fontFamily: 'inherit' }}
127
+ >
128
+ {(appSettings.heartbeatShowAlerts ?? true) ? 'On' : 'Off'}
129
+ </button>
130
+ </div>
131
+ <div>
132
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Delivery Target</label>
133
+ <input
134
+ type="text"
135
+ value={appSettings.heartbeatTarget || ''}
136
+ onChange={(e) => patchSettings({ heartbeatTarget: e.target.value || null })}
137
+ placeholder="none, last, or channel ID"
138
+ className={inputClass}
139
+ style={{ fontFamily: 'inherit' }}
140
+ />
141
+ </div>
142
+ </div>
143
+
144
+ <div>
145
+ <p className="text-[11px] text-text-3/60 mt-2">
146
+ Internal ping text used for ongoing sessions. Leave blank to use the default.
147
+ </p>
148
+ <div className="mt-4 flex items-center gap-2.5">
149
+ <button
150
+ onClick={handleDisableAllHeartbeats}
151
+ disabled={disablingHeartbeats}
152
+ className="px-3.5 py-2 rounded-[10px] border border-rose-400/25 bg-rose-500/10 text-rose-300 hover:bg-rose-500/16 transition-colors cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed text-[12px] font-600"
153
+ style={{ fontFamily: 'inherit' }}
154
+ >
155
+ {disablingHeartbeats ? 'Stopping\u2026' : 'Stop All Session Heartbeats'}
156
+ </button>
157
+ <span className="text-[11px] text-text-3/70">
158
+ Disables heartbeat on every session and cancels queued heartbeat runs.
159
+ </span>
160
+ </div>
161
+ {heartbeatBulkNotice && (
162
+ <p className="text-[11px] text-text-3/70 mt-2">{heartbeatBulkNotice}</p>
163
+ )}
164
+ </div>
165
+ </div>
166
+ </div>
167
+ )
168
+ }
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import type { SettingsSectionProps } from './types'
4
+
5
+ export function MemorySection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ return (
7
+ <div className="mb-10">
8
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
9
+ Memory Retrieval
10
+ </h3>
11
+ <p className="text-[12px] text-text-3 mb-5">
12
+ Guardrails for memory graph traversal and lookup fan-out. These limits are enforced server-side.
13
+ </p>
14
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
15
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
16
+ <div>
17
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
18
+ Reference Depth
19
+ </label>
20
+ <input
21
+ type="number"
22
+ min={0}
23
+ max={12}
24
+ value={appSettings.memoryReferenceDepth ?? appSettings.memoryMaxDepth ?? 3}
25
+ onChange={(e) => {
26
+ const n = Number.parseInt(e.target.value, 10)
27
+ const depth = Number.isFinite(n) ? Math.max(0, Math.min(12, n)) : 3
28
+ patchSettings({ memoryReferenceDepth: depth, memoryMaxDepth: depth })
29
+ }}
30
+ className={inputClass}
31
+ style={{ fontFamily: 'inherit' }}
32
+ />
33
+ <p className="text-[11px] text-text-3/60 mt-2">How far linked memory traversal can go.</p>
34
+ </div>
35
+ <div>
36
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
37
+ Max Per Lookup
38
+ </label>
39
+ <input
40
+ type="number"
41
+ min={1}
42
+ max={200}
43
+ value={appSettings.maxMemoriesPerLookup ?? appSettings.memoryMaxPerLookup ?? 20}
44
+ onChange={(e) => {
45
+ const n = Number.parseInt(e.target.value, 10)
46
+ const perLookup = Number.isFinite(n) ? Math.max(1, Math.min(200, n)) : 20
47
+ patchSettings({ maxMemoriesPerLookup: perLookup, memoryMaxPerLookup: perLookup })
48
+ }}
49
+ className={inputClass}
50
+ style={{ fontFamily: 'inherit' }}
51
+ />
52
+ <p className="text-[11px] text-text-3/60 mt-2">Total memories returned in one retrieval call.</p>
53
+ </div>
54
+ <div>
55
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
56
+ Max Linked Expansion
57
+ </label>
58
+ <input
59
+ type="number"
60
+ min={0}
61
+ max={1000}
62
+ value={appSettings.maxLinkedMemoriesExpanded ?? 60}
63
+ onChange={(e) => {
64
+ const n = Number.parseInt(e.target.value, 10)
65
+ const linked = Number.isFinite(n) ? Math.max(0, Math.min(1000, n)) : 60
66
+ patchSettings({ maxLinkedMemoriesExpanded: linked })
67
+ }}
68
+ className={inputClass}
69
+ style={{ fontFamily: 'inherit' }}
70
+ />
71
+ <p className="text-[11px] text-text-3/60 mt-2">Caps how many linked nodes can be expanded per lookup.</p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
@@ -0,0 +1,108 @@
1
+ 'use client'
2
+
3
+ import { useAppStore } from '@/stores/use-app-store'
4
+ import type { SettingsSectionProps } from './types'
5
+
6
+ const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
7
+
8
+ export function OrchestratorSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
9
+ const providers = useAppStore((s) => s.providers)
10
+ const credentials = useAppStore((s) => s.credentials)
11
+ const credList = Object.values(credentials)
12
+
13
+ const lgProviders = providers.filter((p) => !NON_LANGGRAPH_PROVIDER_IDS.has(String(p.id)))
14
+ const hasConfiguredLgProvider = !!appSettings.langGraphProvider && lgProviders.some((p) => p.id === appSettings.langGraphProvider)
15
+ const lgProvider = hasConfiguredLgProvider ? appSettings.langGraphProvider! : (lgProviders[0]?.id || 'anthropic')
16
+ const lgProviderInfo = lgProviders.find((p) => p.id === lgProvider) || providers.find((p) => p.id === lgProvider)
17
+ const lgCredentials = credList.filter((c) => c.provider === lgProvider)
18
+
19
+ return (
20
+ <div className="mb-10">
21
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
22
+ Orchestrator Engine
23
+ </h3>
24
+ <p className="text-[12px] text-text-3 mb-5">
25
+ The LLM provider used by orchestrators for tool calling, agent generation, and task delegation.
26
+ </p>
27
+
28
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
29
+ {/* Provider picker */}
30
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Provider</label>
31
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2 mb-5">
32
+ {lgProviders.map((p) => (
33
+ <button
34
+ key={p.id}
35
+ onClick={() => patchSettings({ langGraphProvider: p.id, langGraphModel: '', langGraphCredentialId: null, langGraphEndpoint: null })}
36
+ className={`py-3 px-3 rounded-[12px] text-center cursor-pointer transition-all text-[13px] font-600 border
37
+ ${lgProvider === p.id
38
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
39
+ : 'bg-bg border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
40
+ style={{ fontFamily: 'inherit' }}
41
+ >
42
+ {p.name}
43
+ </button>
44
+ ))}
45
+ </div>
46
+ {lgProviders.length === 0 && (
47
+ <p className="text-[12px] text-text-3/60 mb-5">
48
+ No orchestration-compatible providers available. Add an API provider in Providers.
49
+ </p>
50
+ )}
51
+
52
+ {/* Model picker */}
53
+ {lgProviderInfo && lgProviderInfo.models.length > 0 && (
54
+ <div className="mb-5">
55
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Model</label>
56
+ <select
57
+ value={appSettings.langGraphModel || lgProviderInfo.models[0]}
58
+ onChange={(e) => patchSettings({ langGraphModel: e.target.value })}
59
+ className={`${inputClass} appearance-none cursor-pointer`}
60
+ style={{ fontFamily: 'inherit' }}
61
+ >
62
+ {lgProviderInfo.models.map((m) => (
63
+ <option key={m} value={m}>{m}</option>
64
+ ))}
65
+ </select>
66
+ </div>
67
+ )}
68
+
69
+ {(lgProviderInfo?.requiresEndpoint || !!appSettings.langGraphEndpoint) && (
70
+ <div className="mb-5">
71
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Endpoint Override</label>
72
+ <input
73
+ type="text"
74
+ value={appSettings.langGraphEndpoint || ''}
75
+ onChange={(e) => patchSettings({ langGraphEndpoint: e.target.value || null })}
76
+ placeholder={lgProviderInfo?.defaultEndpoint || 'https://api.example.com/v1'}
77
+ className={inputClass}
78
+ style={{ fontFamily: 'inherit' }}
79
+ />
80
+ <p className="text-[11px] text-text-3/60 mt-2">Leave empty to use the provider default endpoint.</p>
81
+ </div>
82
+ )}
83
+
84
+ {/* API Key picker */}
85
+ <div>
86
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
87
+ {lgCredentials.length > 0 ? (
88
+ <select
89
+ value={appSettings.langGraphCredentialId || ''}
90
+ onChange={(e) => patchSettings({ langGraphCredentialId: e.target.value || null })}
91
+ className={`${inputClass} appearance-none cursor-pointer`}
92
+ style={{ fontFamily: 'inherit' }}
93
+ >
94
+ <option value="">Select a key...</option>
95
+ {lgCredentials.map((c) => (
96
+ <option key={c.id} value={c.id}>{c.name}</option>
97
+ ))}
98
+ </select>
99
+ ) : (
100
+ <p className="text-[12px] text-text-3/60">
101
+ No {lgProvider} API keys configured. Add one below in the Providers section.
102
+ </p>
103
+ )}
104
+ </div>
105
+ </div>
106
+ </div>
107
+ )
108
+ }
@@ -0,0 +1,181 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createCredential, deleteCredential } from '@/lib/sessions'
6
+ import { toast } from 'sonner'
7
+ import type { ProviderType } from '@/types'
8
+ import type { SettingsSectionProps } from './types'
9
+
10
+ export function ProvidersSection({ inputClass }: SettingsSectionProps) {
11
+ const providers = useAppStore((s) => s.providers)
12
+ const credentials = useAppStore((s) => s.credentials)
13
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
14
+
15
+ const credList = Object.values(credentials)
16
+
17
+ const [addProvider, setAddProvider] = useState<ProviderType | null>(null)
18
+ const [newName, setNewName] = useState('')
19
+ const [newKey, setNewKey] = useState('')
20
+ const [deleting, setDeleting] = useState<string | null>(null)
21
+
22
+ const handleAdd = async () => {
23
+ if (!addProvider || !newKey.trim()) return
24
+ try {
25
+ await createCredential(addProvider, newName || `${addProvider} key`, newKey)
26
+ await loadCredentials()
27
+ setAddProvider(null)
28
+ setNewName('')
29
+ setNewKey('')
30
+ toast.success('API key added')
31
+ } catch (err: unknown) {
32
+ toast.error(err instanceof Error ? err.message : 'Failed to add API key')
33
+ }
34
+ }
35
+
36
+ const handleDelete = async (id: string) => {
37
+ try {
38
+ await deleteCredential(id)
39
+ await loadCredentials()
40
+ setDeleting(null)
41
+ toast.success('API key deleted')
42
+ } catch (err: unknown) {
43
+ toast.error(err instanceof Error ? err.message : 'Failed to delete API key')
44
+ }
45
+ }
46
+
47
+ return (
48
+ <>
49
+ {/* Providers */}
50
+ <div className="mb-8">
51
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-5">
52
+ Providers
53
+ </h3>
54
+ <div className="space-y-4">
55
+ {providers.map((p) => {
56
+ const providerCreds = credList.filter((c) => c.provider === p.id)
57
+ return (
58
+ <div key={p.id} className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
59
+ <div className="flex items-center justify-between mb-3">
60
+ <span className="font-display text-[17px] font-600 tracking-[-0.01em]">{p.name}</span>
61
+ <span className={`text-[12px] font-600 px-3 py-1 rounded-[8px]
62
+ ${p.requiresApiKey
63
+ ? providerCreds.length > 0 ? 'text-success bg-success/[0.1]' : 'text-text-3 bg-white/[0.04]'
64
+ : 'text-success bg-success/[0.1]'}`}>
65
+ {p.requiresApiKey
66
+ ? providerCreds.length > 0 ? 'Connected' : 'No key'
67
+ : p.optionalApiKey
68
+ ? providerCreds.length > 0 ? 'Local + Cloud' : 'Local'
69
+ : p.requiresEndpoint ? 'Local' : 'Built-in'}
70
+ </span>
71
+ </div>
72
+ <div className="text-[13px] text-text-2/50 font-mono">
73
+ {p.models.slice(0, 3).join(', ')}
74
+ {p.models.length > 3 && ` +${p.models.length - 3} more`}
75
+ </div>
76
+
77
+ {(p.requiresApiKey || p.optionalApiKey) && providerCreds.length > 0 && (
78
+ <div className="mt-5 space-y-2.5">
79
+ {providerCreds.map((cred) => (
80
+ <div key={cred.id} className="flex items-center gap-3 py-3 px-4 rounded-[12px] bg-bg border border-white/[0.06]">
81
+ <span className="text-[14px] font-500 flex-1 truncate">{cred.name}</span>
82
+ {deleting === cred.id ? (
83
+ <div className="flex gap-2">
84
+ <button
85
+ onClick={() => setDeleting(null)}
86
+ className="px-3 py-1.5 text-[13px] font-600 bg-transparent border-none text-text-3 cursor-pointer hover:text-text-2 transition-colors"
87
+ style={{ fontFamily: 'inherit' }}
88
+ >
89
+ Keep
90
+ </button>
91
+ <button
92
+ onClick={() => handleDelete(cred.id)}
93
+ className="px-3 py-1.5 text-[13px] font-600 bg-danger text-white border-none cursor-pointer rounded-[8px] transition-colors hover:brightness-110"
94
+ style={{ fontFamily: 'inherit' }}
95
+ >
96
+ Delete
97
+ </button>
98
+ </div>
99
+ ) : (
100
+ <button
101
+ onClick={() => setDeleting(cred.id)}
102
+ className="px-3 py-1.5 text-[13px] font-500 bg-transparent border-none text-text-3 cursor-pointer hover:text-danger transition-colors"
103
+ style={{ fontFamily: 'inherit' }}
104
+ >
105
+ Remove
106
+ </button>
107
+ )}
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )}
112
+
113
+ {(p.requiresApiKey || p.optionalApiKey) && (
114
+ <button
115
+ onClick={() => setAddProvider(p.id)}
116
+ className="mt-5 w-full py-3 rounded-[12px] border border-dashed border-white/[0.1]
117
+ bg-transparent text-text-3 text-[13px] font-600 cursor-pointer
118
+ hover:border-accent-bright/30 hover:text-accent-bright hover:bg-accent-soft transition-all duration-200"
119
+ style={{ fontFamily: 'inherit' }}
120
+ >
121
+ + Add API Key{p.optionalApiKey && !p.requiresApiKey ? ' (for cloud)' : ''}
122
+ </button>
123
+ )}
124
+
125
+ {p.requiresEndpoint && (
126
+ <div className="mt-5 text-[13px] text-text-3/50 font-mono">
127
+ Endpoint: {(p as any).defaultEndpoint || 'http://localhost:11434'}
128
+ </div>
129
+ )}
130
+ </div>
131
+ )
132
+ })}
133
+ </div>
134
+ </div>
135
+
136
+ {/* Add key form */}
137
+ {addProvider && (
138
+ <div className="mb-8 p-6 rounded-[18px] bg-surface border border-white/[0.06]">
139
+ <div className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-4">
140
+ New {providers.find((p) => p.id === addProvider)?.name} API Key
141
+ </div>
142
+ <div className="space-y-4">
143
+ <input
144
+ type="text"
145
+ value={newName}
146
+ onChange={(e) => setNewName(e.target.value)}
147
+ placeholder="Key name (optional)"
148
+ className={inputClass}
149
+ style={{ fontFamily: 'inherit' }}
150
+ />
151
+ <input
152
+ type="password"
153
+ value={newKey}
154
+ onChange={(e) => setNewKey(e.target.value)}
155
+ placeholder="sk-..."
156
+ className={inputClass}
157
+ style={{ fontFamily: 'inherit' }}
158
+ />
159
+ <div className="flex gap-3 pt-2">
160
+ <button
161
+ onClick={() => { setAddProvider(null); setNewName(''); setNewKey('') }}
162
+ className="flex-1 py-3 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 transition-colors"
163
+ style={{ fontFamily: 'inherit' }}
164
+ >
165
+ Cancel
166
+ </button>
167
+ <button
168
+ onClick={handleAdd}
169
+ disabled={!newKey.trim()}
170
+ className="flex-1 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
171
+ style={{ fontFamily: 'inherit' }}
172
+ >
173
+ Save Key
174
+ </button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ )}
179
+ </>
180
+ )
181
+ }