@swarmclawai/swarmclaw 0.3.1 → 0.4.5

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 (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -1,5 +1,16 @@
1
1
  import type { Connector, ConnectorPlatform, Session } from '@/types'
2
2
  import { cn } from '@/lib/utils'
3
+ import { BsMicrosoftTeams } from 'react-icons/bs'
4
+ import {
5
+ SiApple,
6
+ SiDiscord,
7
+ SiGooglechat,
8
+ SiMatrix,
9
+ SiSignal,
10
+ SiSlack,
11
+ SiTelegram,
12
+ SiWhatsapp,
13
+ } from 'react-icons/si'
3
14
 
4
15
  export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string; color: string }> = {
5
16
  discord: { label: 'Discord', color: '#5865F2' },
@@ -7,6 +18,7 @@ export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string;
7
18
  slack: { label: 'Slack', color: '#4A154B' },
8
19
  whatsapp: { label: 'WhatsApp', color: '#25D366' },
9
20
  openclaw: { label: 'OpenClaw', color: '#F97316' },
21
+ bluebubbles: { label: 'BlueBubbles', color: '#2E89FF' },
10
22
  signal: { label: 'Signal', color: '#3A76F0' },
11
23
  teams: { label: 'Teams', color: '#6264A7' },
12
24
  googlechat: { label: 'Google Chat', color: '#00AC47' },
@@ -45,29 +57,23 @@ export function ConnectorPlatformIcon({
45
57
  }: ConnectorPlatformIconProps) {
46
58
  switch (platform) {
47
59
  case 'discord':
48
- return (
49
- <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
50
- <path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
51
- </svg>
52
- )
60
+ return <SiDiscord size={size} className={className} />
53
61
  case 'telegram':
54
- return (
55
- <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
56
- <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
57
- </svg>
58
- )
62
+ return <SiTelegram size={size} className={className} />
59
63
  case 'slack':
60
- return (
61
- <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
62
- <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" />
63
- </svg>
64
- )
64
+ return <SiSlack size={size} className={className} />
65
65
  case 'whatsapp':
66
- return (
67
- <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
68
- <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" />
69
- </svg>
70
- )
66
+ return <SiWhatsapp size={size} className={className} />
67
+ case 'bluebubbles':
68
+ return <SiApple size={size} className={className} />
69
+ case 'signal':
70
+ return <SiSignal size={size} className={className} />
71
+ case 'googlechat':
72
+ return <SiGooglechat size={size} className={className} />
73
+ case 'matrix':
74
+ return <SiMatrix size={size} className={className} />
75
+ case 'teams':
76
+ return <BsMicrosoftTeams size={size} className={className} />
71
77
  case 'openclaw':
72
78
  return (
73
79
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
@@ -0,0 +1,148 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+
7
+ interface ModelComboboxProps {
8
+ providerId: string
9
+ value: string
10
+ onChange: (model: string) => void
11
+ models: string[]
12
+ defaultModels?: string[]
13
+ className?: string
14
+ }
15
+
16
+ export function ModelCombobox({
17
+ providerId,
18
+ value,
19
+ onChange,
20
+ models,
21
+ defaultModels = [],
22
+ className,
23
+ }: ModelComboboxProps) {
24
+ const [open, setOpen] = useState(false)
25
+ const [query, setQuery] = useState('')
26
+ const inputRef = useRef<HTMLInputElement>(null)
27
+ const containerRef = useRef<HTMLDivElement>(null)
28
+ const loadProviders = useAppStore((s) => s.loadProviders)
29
+
30
+ const filtered = query
31
+ ? models.filter((m) => m.toLowerCase().includes(query.toLowerCase()))
32
+ : models
33
+
34
+ const isCustom = (m: string) => defaultModels.length > 0 && !defaultModels.includes(m)
35
+ const showAdd = query && !models.some((m) => m.toLowerCase() === query.toLowerCase())
36
+
37
+ const persistModels = useCallback(async (next: string[]) => {
38
+ await api('PUT', `/providers/${providerId}/models`, { models: next })
39
+ await loadProviders()
40
+ }, [providerId, loadProviders])
41
+
42
+ const addModel = useCallback(async (name: string) => {
43
+ const next = [...models, name]
44
+ await persistModels(next)
45
+ onChange(name)
46
+ setQuery('')
47
+ setOpen(false)
48
+ }, [models, persistModels, onChange])
49
+
50
+ const removeModel = useCallback(async (name: string, e: React.MouseEvent) => {
51
+ e.stopPropagation()
52
+ const next = models.filter((m) => m !== name)
53
+ if (value === name) onChange(next[0] || '')
54
+ if (next.length === defaultModels.length && next.every((m) => defaultModels.includes(m))) {
55
+ await api('DELETE', `/providers/${providerId}/models`)
56
+ } else {
57
+ await persistModels(next)
58
+ }
59
+ await loadProviders()
60
+ }, [models, defaultModels, value, onChange, providerId, persistModels, loadProviders])
61
+
62
+ const selectModel = useCallback((m: string) => {
63
+ onChange(m)
64
+ setQuery('')
65
+ setOpen(false)
66
+ }, [onChange])
67
+
68
+ useEffect(() => {
69
+ const handler = (e: MouseEvent) => {
70
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
71
+ setOpen(false)
72
+ setQuery('')
73
+ }
74
+ }
75
+ document.addEventListener('mousedown', handler)
76
+ return () => document.removeEventListener('mousedown', handler)
77
+ }, [])
78
+
79
+ return (
80
+ <div ref={containerRef} className="relative">
81
+ <div
82
+ className={`flex items-center ${className || ''}`}
83
+ onClick={() => {
84
+ setOpen(true)
85
+ setTimeout(() => inputRef.current?.focus(), 0)
86
+ }}
87
+ >
88
+ <input
89
+ ref={inputRef}
90
+ type="text"
91
+ value={open ? query : value}
92
+ onChange={(e) => {
93
+ setQuery(e.target.value)
94
+ if (!open) setOpen(true)
95
+ }}
96
+ onFocus={() => setOpen(true)}
97
+ placeholder={value || 'Select model…'}
98
+ className="w-full bg-transparent outline-none text-inherit placeholder:text-text-3/50"
99
+ style={{ fontFamily: 'inherit' }}
100
+ />
101
+ <svg className="w-4 h-4 text-text-3 shrink-0 ml-2" viewBox="0 0 16 16" fill="none">
102
+ <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
103
+ </svg>
104
+ </div>
105
+
106
+ {open && (
107
+ <div className="absolute z-50 top-full left-0 right-0 mt-1 max-h-[240px] overflow-y-auto rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-xl">
108
+ {filtered.map((m) => (
109
+ <div
110
+ key={m}
111
+ onClick={() => selectModel(m)}
112
+ className={`flex items-center justify-between px-3 py-2 text-[14px] cursor-pointer transition-colors hover:bg-white/[0.04] ${m === value ? 'text-accent-bright' : 'text-text'}`}
113
+ >
114
+ <span className="truncate">{m}</span>
115
+ {isCustom(m) && (
116
+ <button
117
+ onClick={(e) => removeModel(m, e)}
118
+ className="ml-2 p-0.5 rounded hover:bg-white/[0.08] text-text-3 hover:text-red-400 transition-colors shrink-0"
119
+ title="Remove custom model"
120
+ >
121
+ <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none">
122
+ <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
123
+ </svg>
124
+ </button>
125
+ )}
126
+ </div>
127
+ ))}
128
+
129
+ {showAdd && (
130
+ <div
131
+ onClick={() => addModel(query.trim())}
132
+ className="flex items-center gap-2 px-3 py-2 text-[14px] cursor-pointer transition-colors hover:bg-white/[0.04] text-accent-bright border-t border-white/[0.06]"
133
+ >
134
+ <svg className="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
135
+ <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
136
+ </svg>
137
+ <span className="truncate">Add &ldquo;{query.trim()}&rdquo;</span>
138
+ </div>
139
+ )}
140
+
141
+ {filtered.length === 0 && !showAdd && (
142
+ <div className="px-3 py-2 text-[14px] text-text-3">No models found</div>
143
+ )}
144
+ </div>
145
+ )}
146
+ </div>
147
+ )
148
+ }
@@ -26,7 +26,7 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
26
26
  `Stopped heartbeat on ${result.updatedSessions} session(s); cancelled ${result.cancelledQueued} queued run(s), aborted ${result.abortedRunning} running run(s).`,
27
27
  )
28
28
  } catch (err: any) {
29
- setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all sessions.')
29
+ setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all agents.')
30
30
  } finally {
31
31
  setDisablingHeartbeats(false)
32
32
  }
@@ -35,49 +35,20 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
35
35
  return (
36
36
  <div className="mb-10">
37
37
  <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
38
- Heartbeat
38
+ Heartbeat Defaults
39
39
  </h3>
40
40
  <p className="text-[12px] text-text-3 mb-5">
41
- Configure ongoing heartbeat checks for long-lived sessions.
41
+ Global defaults inherited by agents. Enable heartbeat and set interval/model per-agent in the agent editor.
42
42
  </p>
43
43
  <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
44
44
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
45
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>
46
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Default Prompt</label>
62
47
  <input
63
48
  type="text"
64
49
  value={appSettings.heartbeatPrompt || ''}
65
50
  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"
51
+ placeholder="Leave blank for built-in default"
81
52
  className={inputClass}
82
53
  style={{ fontFamily: 'inherit' }}
83
54
  />
@@ -142,20 +113,17 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
142
113
  </div>
143
114
 
144
115
  <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">
116
+ <div className="flex items-center gap-2.5">
149
117
  <button
150
118
  onClick={handleDisableAllHeartbeats}
151
119
  disabled={disablingHeartbeats}
152
120
  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
121
  style={{ fontFamily: 'inherit' }}
154
122
  >
155
- {disablingHeartbeats ? 'Stopping\u2026' : 'Stop All Session Heartbeats'}
123
+ {disablingHeartbeats ? 'Stopping\u2026' : 'Stop All Heartbeats'}
156
124
  </button>
157
125
  <span className="text-[11px] text-text-3/70">
158
- Disables heartbeat on every session and cancels queued heartbeat runs.
126
+ Disables heartbeat on every agent and cancels queued runs.
159
127
  </span>
160
128
  </div>
161
129
  {heartbeatBulkNotice && (
@@ -1,10 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import { useAppStore } from '@/stores/use-app-store'
4
+ import { ModelCombobox } from '@/components/shared/model-combobox'
5
+ import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
4
6
  import type { SettingsSectionProps } from './types'
5
7
 
6
- const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
7
-
8
8
  export function OrchestratorSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
9
9
  const providers = useAppStore((s) => s.providers)
10
10
  const credentials = useAppStore((s) => s.credentials)
@@ -53,16 +53,14 @@ export function OrchestratorSection({ appSettings, patchSettings, inputClass }:
53
53
  {lgProviderInfo && lgProviderInfo.models.length > 0 && (
54
54
  <div className="mb-5">
55
55
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Model</label>
56
- <select
56
+ <ModelCombobox
57
+ providerId={lgProviderInfo.id}
57
58
  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>
59
+ onChange={(m) => patchSettings({ langGraphModel: m })}
60
+ models={lgProviderInfo.models}
61
+ defaultModels={lgProviderInfo.defaultModels}
62
+ className={`${inputClass} cursor-pointer`}
63
+ />
66
64
  </div>
67
65
  )}
68
66
 
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import type { SettingsSectionProps } from './types'
4
+
5
+ export function WebSearchSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ const provider = appSettings.webSearchProvider || 'duckduckgo'
7
+
8
+ return (
9
+ <div className="mb-10">
10
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
11
+ Web Search
12
+ </h3>
13
+ <p className="text-[12px] text-text-3 mb-5">
14
+ Choose which search engine agents use for the <code className="text-[11px] font-mono text-text-2">web_search</code> tool.
15
+ </p>
16
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
17
+ <div className="mb-5">
18
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Search Provider</label>
19
+ <select
20
+ value={provider}
21
+ onChange={(e) => patchSettings({ webSearchProvider: e.target.value as typeof provider })}
22
+ className={inputClass}
23
+ style={{ fontFamily: 'inherit' }}
24
+ >
25
+ <option value="duckduckgo">DuckDuckGo (default, no key required)</option>
26
+ <option value="google">Google (scraping, no key required)</option>
27
+ <option value="bing">Bing (scraping, no key required)</option>
28
+ <option value="searxng">SearXNG (self-hosted, no key required)</option>
29
+ <option value="tavily">Tavily (requires API key in Secrets)</option>
30
+ <option value="brave">Brave Search (requires API key in Secrets)</option>
31
+ </select>
32
+ </div>
33
+
34
+ {provider === 'searxng' && (
35
+ <div>
36
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">SearXNG URL</label>
37
+ <input
38
+ type="text"
39
+ value={appSettings.searxngUrl || ''}
40
+ onChange={(e) => patchSettings({ searxngUrl: e.target.value || undefined })}
41
+ placeholder="http://localhost:8080"
42
+ className={inputClass}
43
+ style={{ fontFamily: 'inherit' }}
44
+ />
45
+ </div>
46
+ )}
47
+
48
+ {(provider === 'tavily' || provider === 'brave') && (
49
+ <p className="text-[11px] text-text-3/70">
50
+ Add a secret named &quot;{provider}&quot; or &quot;{provider}_api_key&quot; in the Secrets section below.
51
+ </p>
52
+ )}
53
+ </div>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,73 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { inputClass } from './utils'
6
+ import { UserPreferencesSection } from './section-user-preferences'
7
+ import { OrchestratorSection } from './section-orchestrator'
8
+ import { RuntimeLoopSection } from './section-runtime-loop'
9
+ import { CapabilityPolicySection } from './section-capability-policy'
10
+ import { VoiceSection } from './section-voice'
11
+ import { WebSearchSection } from './section-web-search'
12
+ import { HeartbeatSection } from './section-heartbeat'
13
+ import { EmbeddingSection } from './section-embedding'
14
+ import { MemorySection } from './section-memory'
15
+ import { SecretsSection } from './section-secrets'
16
+ import { ProvidersSection } from './section-providers'
17
+ import { PluginManager } from './plugin-manager'
18
+
19
+ export function SettingsPage() {
20
+ const loadProviders = useAppStore((s) => s.loadProviders)
21
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
22
+ const appSettings = useAppStore((s) => s.appSettings)
23
+ const loadSettings = useAppStore((s) => s.loadSettings)
24
+ const updateSettings = useAppStore((s) => s.updateSettings)
25
+ const loadSecrets = useAppStore((s) => s.loadSecrets)
26
+ const loadAgents = useAppStore((s) => s.loadAgents)
27
+ const credentials = useAppStore((s) => s.credentials)
28
+
29
+ useEffect(() => {
30
+ loadProviders()
31
+ loadCredentials()
32
+ loadSettings()
33
+ loadSecrets()
34
+ loadAgents()
35
+ }, [])
36
+
37
+ const credList = Object.values(credentials)
38
+ const patchSettings = updateSettings
39
+
40
+ return (
41
+ <div className="flex-1 flex flex-col h-full overflow-y-auto">
42
+ <div className="w-full max-w-3xl mx-auto px-6 py-8">
43
+ <div className="mb-10">
44
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">Settings</h2>
45
+ <p className="text-[14px] text-text-3">Manage providers, API keys & orchestrator engine</p>
46
+ </div>
47
+
48
+ <UserPreferencesSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
49
+ <OrchestratorSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
50
+ <RuntimeLoopSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
51
+ <CapabilityPolicySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
52
+ <WebSearchSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
53
+ <VoiceSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
54
+ <HeartbeatSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
55
+ <EmbeddingSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} credList={credList} />
56
+ <MemorySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
57
+ <SecretsSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
58
+ <ProvidersSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
59
+
60
+ <div className="mb-10">
61
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
62
+ Plugins
63
+ </h3>
64
+ <p className="text-[12px] text-text-3 mb-5">
65
+ Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
66
+ <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
67
+ </p>
68
+ <PluginManager />
69
+ </div>
70
+ </div>
71
+ </div>
72
+ )
73
+ }