@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -164,39 +164,33 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
164
164
  const filteredEmojis = useMemo(() => {
165
165
  if (!search.trim()) return null
166
166
  const q = search.toLowerCase()
167
- // Simple search: match against category names or just return all emojis that are visible
168
- const results: string[] = []
167
+ const directEmojiMatches: string[] = []
169
168
  const seen = new Set<string>()
170
169
  for (const cat of CATEGORIES) {
171
170
  if (cat.id === 'frequent') continue
172
171
  for (const emoji of cat.emojis) {
173
- if (!seen.has(emoji)) {
174
- seen.add(emoji)
175
- results.push(emoji)
176
- }
172
+ if (seen.has(emoji)) continue
173
+ seen.add(emoji)
174
+ if (emoji.includes(search.trim())) directEmojiMatches.push(emoji)
177
175
  }
178
176
  }
179
- // For basic emoji search, filter by category label matching
180
- // Since emoji don't have text names in this simple implementation,
181
- // we filter categories that match and show all their emojis
177
+ if (directEmojiMatches.length > 0) return directEmojiMatches
178
+
179
+ // This lightweight picker only understands category labels, not emoji names.
182
180
  const matchingCats = CATEGORIES.filter(
183
181
  (c) => c.id !== 'frequent' && c.label.toLowerCase().includes(q)
184
182
  )
185
- if (matchingCats.length > 0) {
186
- const catResults: string[] = []
187
- const catSeen = new Set<string>()
188
- for (const cat of matchingCats) {
189
- for (const emoji of cat.emojis) {
190
- if (!catSeen.has(emoji)) {
191
- catSeen.add(emoji)
192
- catResults.push(emoji)
193
- }
183
+ const catResults: string[] = []
184
+ const catSeen = new Set<string>()
185
+ for (const cat of matchingCats) {
186
+ for (const emoji of cat.emojis) {
187
+ if (!catSeen.has(emoji)) {
188
+ catSeen.add(emoji)
189
+ catResults.push(emoji)
194
190
  }
195
191
  }
196
- return catResults
197
192
  }
198
- // If no category match, just return all emojis (user can visually scan)
199
- return results
193
+ return catResults
200
194
  }, [search])
201
195
 
202
196
  return (
@@ -212,9 +206,14 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
212
206
  type="text"
213
207
  value={search}
214
208
  onChange={(e) => setSearch(e.target.value)}
215
- placeholder="Search emoji..."
209
+ placeholder="Filter by category or paste emoji..."
216
210
  className="w-full px-2.5 py-1.5 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
217
211
  />
212
+ {search.trim() && (
213
+ <p className="mt-1 px-0.5 text-[10px] text-text-3/55">
214
+ This picker filters category labels rather than emoji names.
215
+ </p>
216
+ )}
218
217
  </div>
219
218
 
220
219
  {/* Category tabs */}
@@ -238,17 +237,23 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
238
237
  {/* Emoji grid */}
239
238
  <div className="px-2 pb-2 max-h-[220px] overflow-y-auto">
240
239
  {search.trim() ? (
241
- <div className="grid grid-cols-8 gap-0.5">
242
- {filteredEmojis?.map((emoji, i) => (
243
- <button
244
- key={`${emoji}-${i}`}
245
- onClick={() => onSelect(emoji)}
246
- className="w-[34px] h-[34px] flex items-center justify-center rounded-[6px] hover:bg-white/[0.08] transition-all cursor-pointer text-[18px]"
247
- >
248
- {emoji}
249
- </button>
250
- ))}
251
- </div>
240
+ filteredEmojis && filteredEmojis.length > 0 ? (
241
+ <div className="grid grid-cols-8 gap-0.5">
242
+ {filteredEmojis.map((emoji, i) => (
243
+ <button
244
+ key={`${emoji}-${i}`}
245
+ onClick={() => onSelect(emoji)}
246
+ className="w-[34px] h-[34px] flex items-center justify-center rounded-[6px] hover:bg-white/[0.08] transition-all cursor-pointer text-[18px]"
247
+ >
248
+ {emoji}
249
+ </button>
250
+ ))}
251
+ </div>
252
+ ) : (
253
+ <div className="px-2 py-6 text-center text-[11px] text-text-3/60">
254
+ No category matches. Try terms like <span className="text-text-3">food</span>, <span className="text-text-3">travel</span>, or paste an emoji.
255
+ </div>
256
+ )
252
257
  ) : (
253
258
  CATEGORIES.filter((c) => c.id === activeCategory).map((cat) => (
254
259
  <div key={cat.id}>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useState } from 'react'
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatroomStore } from '@/stores/use-chatroom-store'
6
6
  import { useWs } from '@/hooks/use-ws'
@@ -18,6 +18,24 @@ function relativeTime(ts: number): string {
18
18
  return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
19
19
  }
20
20
 
21
+ type ConnectorGroup = 'needs-setup' | 'attention' | 'healthy'
22
+
23
+ function hasConnectorCredentials(connector: Connector): boolean {
24
+ return connector.platform === 'whatsapp'
25
+ || connector.platform === 'openclaw'
26
+ || connector.platform === 'signal'
27
+ || (connector.platform === 'bluebubbles' && (!!connector.credentialId || !!connector.config?.password))
28
+ || !!connector.credentialId
29
+ }
30
+
31
+ function getConnectorGroup(connector: Connector): ConnectorGroup {
32
+ const missingRoute = !connector.agentId && !connector.chatroomId
33
+ const needsSetup = !hasConnectorCredentials(connector) || !!connector.qrDataUrl || missingRoute
34
+ if (needsSetup) return 'needs-setup'
35
+ if (connector.status === 'running' && !connector.lastError) return 'healthy'
36
+ return 'attention'
37
+ }
38
+
21
39
  export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
22
40
  const connectors = useAppStore((s) => s.connectors)
23
41
  const loadConnectors = useAppStore((s) => s.loadConnectors)
@@ -31,6 +49,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
31
49
  const [reconnecting, setReconnecting] = useState<string | null>(null)
32
50
  const [loaded, setLoaded] = useState(false)
33
51
  const [error, setError] = useState<string | null>(null)
52
+ const [groupFilter, setGroupFilter] = useState<'all' | ConnectorGroup>('all')
34
53
  const openConnector = useCallback((id: string | null) => {
35
54
  setEditingConnectorId(id)
36
55
  setConnectorSheetOpen(true)
@@ -84,7 +103,48 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
84
103
  }
85
104
  }
86
105
 
87
- const list = Object.values(connectors) as Connector[]
106
+ const list = useMemo(() => (
107
+ (Object.values(connectors) as Connector[]).sort((a, b) => {
108
+ const groupOrder: Record<ConnectorGroup, number> = {
109
+ 'needs-setup': 0,
110
+ attention: 1,
111
+ healthy: 2,
112
+ }
113
+ const diff = groupOrder[getConnectorGroup(a)] - groupOrder[getConnectorGroup(b)]
114
+ if (diff !== 0) return diff
115
+ return a.name.localeCompare(b.name)
116
+ })
117
+ ), [connectors])
118
+
119
+ const groupedConnectors = useMemo(() => {
120
+ const groups: Record<ConnectorGroup, Connector[]> = {
121
+ 'needs-setup': [],
122
+ attention: [],
123
+ healthy: [],
124
+ }
125
+ for (const connector of list) {
126
+ groups[getConnectorGroup(connector)].push(connector)
127
+ }
128
+ return groups
129
+ }, [list])
130
+
131
+ const groupMeta: Record<ConnectorGroup, { label: string; description: string; tone: string }> = {
132
+ 'needs-setup': {
133
+ label: 'Needs Setup',
134
+ description: 'Missing credentials, QR scan, or routing target',
135
+ tone: 'text-amber-400',
136
+ },
137
+ attention: {
138
+ label: 'Attention',
139
+ description: 'Configured, but stopped or reporting errors',
140
+ tone: 'text-red-400',
141
+ },
142
+ healthy: {
143
+ label: 'Healthy',
144
+ description: 'Connected and routed correctly',
145
+ tone: 'text-emerald-400',
146
+ },
147
+ }
88
148
 
89
149
  if (!loaded) {
90
150
  return (
@@ -122,6 +182,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
122
182
  const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
123
183
  const isRunning = c.status === 'running'
124
184
  const meta = CONNECTOR_PLATFORM_META[c.platform]
185
+ const group = getConnectorGroup(c)
125
186
  return (
126
187
  <button
127
188
  key={c.id}
@@ -140,7 +201,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
140
201
  </span>
141
202
  </div>
142
203
  <span className={`shrink-0 w-2 h-2 rounded-full ${
143
- isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
204
+ group === 'healthy' ? 'bg-green-400' : group === 'attention' ? 'bg-red-400' : 'bg-amber-400'
144
205
  }`}
145
206
  style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : undefined} />
146
207
  </button>
@@ -158,137 +219,214 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
158
219
  {error}
159
220
  </div>
160
221
  )}
161
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
162
- {list.map((c, idx) => {
163
- const platformLabel = getConnectorPlatformLabel(c.platform)
164
- const agent = c.agentId ? agents[c.agentId] : null
165
- const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
166
- const isRunning = c.status === 'running'
167
- const isToggling = toggling === c.id
168
- const hasCredentials = c.platform === 'whatsapp'
169
- || c.platform === 'openclaw'
170
- || c.platform === 'signal'
171
- || (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
172
- || !!c.credentialId
173
- const lastMsg = c.presence?.lastMessageAt
222
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
223
+ {(Object.entries(groupMeta) as Array<[ConnectorGroup, { label: string; description: string; tone: string }]>).map(([group, meta]) => (
224
+ <button
225
+ key={group}
226
+ onClick={() => setGroupFilter((current) => (current === group ? 'all' : group))}
227
+ className={`rounded-[14px] border px-4 py-3 text-left transition-all cursor-pointer ${
228
+ groupFilter === group
229
+ ? 'border-white/[0.12] bg-white/[0.05]'
230
+ : 'border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04]'
231
+ }`}
232
+ style={{ fontFamily: 'inherit' }}
233
+ >
234
+ <div className={`text-[11px] font-700 uppercase tracking-[0.08em] ${meta.tone}`}>{meta.label}</div>
235
+ <div className={`mt-2 text-[24px] font-display font-700 tracking-[-0.03em] ${meta.tone}`}>{groupedConnectors[group].length}</div>
236
+ <p className="text-[11px] text-text-3/55 mt-1 leading-relaxed">{meta.description}</p>
237
+ </button>
238
+ ))}
239
+ </div>
174
240
 
175
- return (
176
- <div
177
- key={c.id}
178
- role="button"
179
- tabIndex={0}
180
- onClick={() => openConnector(c.id)}
181
- onKeyDown={(e) => {
182
- if (e.key === 'Enter' || e.key === ' ') {
183
- e.preventDefault()
184
- openConnector(c.id)
185
- }
186
- }}
187
- className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] hover:scale-[1.01] text-left w-full"
188
- style={{
189
- fontFamily: 'inherit',
190
- animation: 'spring-in 0.5s var(--ease-spring) both',
191
- animationDelay: `${idx * 0.05}s`
192
- }}
193
- >
194
- {/* Header: platform badge + status */}
195
- <div className="flex items-center gap-3 mb-3">
196
- <ConnectorPlatformBadge platform={c.platform} size={40} iconSize={20} roundedClassName="rounded-[10px]" />
197
- <div className="flex-1 min-w-0">
198
- <div className="flex items-center gap-2">
199
- <span className="text-[14px] font-600 text-text truncate">{c.name}</span>
200
- <span className={`shrink-0 w-2 h-2 rounded-full ${
201
- isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
202
- }`}
203
- style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : c.status === 'error' ? { animation: 'ai-shake 0.5s' } : undefined} />
241
+ <div className="flex flex-wrap gap-2 mb-4">
242
+ {(['all', 'needs-setup', 'attention', 'healthy'] as const).map((group) => (
243
+ <button
244
+ key={group}
245
+ onClick={() => setGroupFilter(group)}
246
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 transition-all cursor-pointer border-none ${
247
+ groupFilter === group
248
+ ? 'bg-accent-soft text-accent-bright'
249
+ : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08] hover:text-text-2'
250
+ }`}
251
+ style={{ fontFamily: 'inherit' }}
252
+ >
253
+ {group === 'all' ? 'All connectors' : groupMeta[group].label}
254
+ </button>
255
+ ))}
256
+ </div>
257
+
258
+ <div className="flex flex-col gap-6">
259
+ {(Object.entries(groupMeta) as Array<[ConnectorGroup, { label: string; description: string; tone: string }]>)
260
+ .filter(([group]) => groupFilter === 'all' || groupFilter === group)
261
+ .map(([group, meta]) => {
262
+ const connectorsForGroup = groupedConnectors[group]
263
+ if (connectorsForGroup.length === 0) return null
264
+ return (
265
+ <section key={group}>
266
+ <div className="flex items-end justify-between gap-3 mb-3">
267
+ <div>
268
+ <h2 className={`text-[12px] font-700 uppercase tracking-[0.1em] ${meta.tone}`}>{meta.label}</h2>
269
+ <p className="text-[12px] text-text-3/55 mt-1">{meta.description}</p>
204
270
  </div>
205
- <span className="text-[11px] text-text-3 block">
206
- {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
207
- {c.qrDataUrl && ' · QR ready'}
208
- </span>
271
+ <span className="text-[11px] text-text-3/45">{connectorsForGroup.length} connector{connectorsForGroup.length === 1 ? '' : 's'}</span>
209
272
  </div>
210
- </div>
211
273
 
212
- {/* Route target: agent or chatroom */}
213
- <div className="flex items-center gap-2.5 mb-2.5 px-0.5">
214
- {chatroom ? (
215
- <>
216
- <div className="w-6 h-6 rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
217
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
218
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
219
- </svg>
220
- </div>
221
- <div className="flex-1 min-w-0">
222
- <span className="text-[12px] font-600 text-text-2 block truncate">{chatroom.name}</span>
223
- <span className="text-[10px] text-text-3/60 block">
224
- {chatroom.agentIds.length} agent{chatroom.agentIds.length !== 1 ? 's' : ''}
225
- {chatroom.chatMode === 'parallel' ? ' · parallel' : ' · sequential'}
226
- </span>
227
- </div>
228
- </>
229
- ) : agent ? (
230
- <>
231
- <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
232
- <div className="flex-1 min-w-0">
233
- <span className="text-[12px] font-600 text-text-2 block truncate">{agent.name}</span>
234
- <span className="text-[10px] text-text-3/60 block">{agent.provider}/{agent.model}</span>
235
- </div>
236
- </>
237
- ) : (
238
- <span className="text-[11px] text-text-3/50">{platformLabel}</span>
239
- )}
240
- </div>
274
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
275
+ {connectorsForGroup.map((c, idx) => {
276
+ const platformLabel = getConnectorPlatformLabel(c.platform)
277
+ const agent = c.agentId ? agents[c.agentId] : null
278
+ const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
279
+ const isRunning = c.status === 'running'
280
+ const isToggling = toggling === c.id
281
+ const hasCredentials = hasConnectorCredentials(c)
282
+ const lastMsg = c.presence?.lastMessageAt
283
+ const missingRoute = !chatroom && !agent
284
+ const issues = [
285
+ !hasCredentials ? { label: 'Credentials missing', tone: 'text-red-400 bg-red-500/10' } : null,
286
+ c.qrDataUrl ? { label: 'QR required', tone: 'text-amber-400 bg-amber-500/10' } : null,
287
+ missingRoute ? { label: 'Routing missing', tone: 'text-amber-300 bg-amber-500/10' } : null,
288
+ ].filter(Boolean) as Array<{ label: string; tone: string }>
289
+
290
+ return (
291
+ <div
292
+ key={c.id}
293
+ role="button"
294
+ tabIndex={0}
295
+ onClick={() => openConnector(c.id)}
296
+ onKeyDown={(e) => {
297
+ if (e.key === 'Enter' || e.key === ' ') {
298
+ e.preventDefault()
299
+ openConnector(c.id)
300
+ }
301
+ }}
302
+ className={`group relative flex flex-col rounded-[14px] border p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] hover:scale-[1.01] text-left w-full ${
303
+ group === 'healthy'
304
+ ? 'border-emerald-500/15 bg-emerald-500/[0.03]'
305
+ : group === 'attention'
306
+ ? 'border-red-500/15 bg-red-500/[0.03]'
307
+ : 'border-amber-500/15 bg-amber-500/[0.03]'
308
+ }`}
309
+ style={{
310
+ fontFamily: 'inherit',
311
+ animation: 'spring-in 0.5s var(--ease-spring) both',
312
+ animationDelay: `${idx * 0.05}s`
313
+ }}
314
+ >
315
+ <div className="flex items-start gap-3 mb-3">
316
+ <ConnectorPlatformBadge platform={c.platform} size={40} iconSize={20} roundedClassName="rounded-[10px]" />
317
+ <div className="flex-1 min-w-0">
318
+ <div className="flex items-center gap-2">
319
+ <span className="text-[14px] font-600 text-text truncate">{c.name}</span>
320
+ <span className={`shrink-0 w-2 h-2 rounded-full ${
321
+ group === 'healthy' ? 'bg-green-400' : group === 'attention' ? 'bg-red-400' : 'bg-amber-400'
322
+ }`}
323
+ style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : c.status === 'error' ? { animation: 'ai-shake 0.5s' } : undefined} />
324
+ </div>
325
+ <div className="flex flex-wrap items-center gap-1.5 mt-1">
326
+ <span className={`px-1.5 py-0.5 rounded-[5px] text-[10px] font-700 uppercase tracking-[0.08em] ${meta.tone} bg-white/[0.05]`}>
327
+ {meta.label}
328
+ </span>
329
+ <span className="text-[11px] text-text-3">
330
+ {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
331
+ </span>
332
+ </div>
333
+ </div>
334
+ </div>
335
+
336
+ <div className="flex items-center gap-2.5 mb-3 px-0.5">
337
+ {chatroom ? (
338
+ <>
339
+ <div className="w-6 h-6 rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
340
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
341
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
342
+ </svg>
343
+ </div>
344
+ <div className="flex-1 min-w-0">
345
+ <span className="text-[12px] font-600 text-text-2 block truncate">{chatroom.name}</span>
346
+ <span className="text-[10px] text-text-3/60 block">
347
+ Room · {chatroom.agentIds.length} agent{chatroom.agentIds.length !== 1 ? 's' : ''}
348
+ </span>
349
+ </div>
350
+ </>
351
+ ) : agent ? (
352
+ <>
353
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
354
+ <div className="flex-1 min-w-0">
355
+ <span className="text-[12px] font-600 text-text-2 block truncate">{agent.name}</span>
356
+ <span className="text-[10px] text-text-3/60 block">Agent route</span>
357
+ </div>
358
+ </>
359
+ ) : (
360
+ <span className="text-[11px] text-amber-300">No routing target yet</span>
361
+ )}
362
+ </div>
241
363
 
242
- {/* Footer: last message time + error */}
243
- <div className="flex items-center gap-2 mt-auto pt-2 border-t border-white/[0.04]">
244
- {c.lastError ? (
245
- <span className="text-[10px] text-red-400 truncate flex-1">
246
- {c.lastError.slice(0, 50)}{c.lastError.length > 50 ? '...' : ''}
247
- </span>
248
- ) : lastMsg ? (
249
- <span className="text-[10px] text-text-3/60 flex-1">Last message {relativeTime(lastMsg)}</span>
250
- ) : (
251
- <span className="text-[10px] text-text-3/40 flex-1">No messages yet</span>
252
- )}
364
+ {issues.length > 0 ? (
365
+ <div className="flex flex-wrap gap-1.5 mb-3">
366
+ {issues.map((issue) => (
367
+ <span key={issue.label} className={`px-2 py-1 rounded-[7px] text-[10px] font-700 ${issue.tone}`}>
368
+ {issue.label}
369
+ </span>
370
+ ))}
371
+ </div>
372
+ ) : (
373
+ <div className="text-[11px] text-text-3/55 mb-3">
374
+ {platformLabel} routed to {chatroom ? 'chatroom' : agent ? 'agent' : 'connector'}.
375
+ </div>
376
+ )}
253
377
 
254
- {/* Action buttons */}
255
- <div className="flex gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
256
- {c.status === 'error' && hasCredentials && (
257
- <button
258
- onClick={(e) => handleReconnect(e, c)}
259
- disabled={reconnecting === c.id}
260
- title="Reconnect"
261
- className="px-2 py-1 rounded-[6px] text-[10px] font-600 transition-all cursor-pointer border-none opacity-0 group-hover:opacity-100 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 disabled:opacity-50"
262
- >
263
- {reconnecting === c.id ? '...' : 'Reconnect'}
264
- </button>
265
- )}
266
- {hasCredentials && (
267
- <button
268
- onClick={(e) => handleToggle(e, c)}
269
- disabled={isToggling}
270
- title={isRunning ? 'Stop' : 'Start'}
271
- className={`w-7 h-7 rounded-[6px] flex items-center justify-center transition-all cursor-pointer border-none ${
272
- isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
273
- } ${isRunning
274
- ? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
275
- : 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
276
- } disabled:opacity-50`}
277
- >
278
- {isToggling ? (
279
- <span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
280
- ) : isRunning ? (
281
- <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>
282
- ) : (
283
- <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 21,12 6,21" /></svg>
284
- )}
285
- </button>
286
- )}
378
+ <div className="flex items-center gap-2 mt-auto pt-2 border-t border-white/[0.04]">
379
+ {c.lastError ? (
380
+ <span className="text-[10px] text-red-400 truncate flex-1">
381
+ {c.lastError.slice(0, 50)}{c.lastError.length > 50 ? '...' : ''}
382
+ </span>
383
+ ) : lastMsg ? (
384
+ <span className="text-[10px] text-text-3/60 flex-1">Last message {relativeTime(lastMsg)}</span>
385
+ ) : (
386
+ <span className="text-[10px] text-text-3/40 flex-1">No messages yet</span>
387
+ )}
388
+
389
+ <div className="flex gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
390
+ {c.status === 'error' && hasCredentials && (
391
+ <button
392
+ onClick={(e) => handleReconnect(e, c)}
393
+ disabled={reconnecting === c.id}
394
+ title="Reconnect"
395
+ className="px-2 py-1 rounded-[6px] text-[10px] font-600 transition-all cursor-pointer border-none opacity-0 group-hover:opacity-100 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 disabled:opacity-50"
396
+ >
397
+ {reconnecting === c.id ? '...' : 'Reconnect'}
398
+ </button>
399
+ )}
400
+ {hasCredentials && (
401
+ <button
402
+ onClick={(e) => handleToggle(e, c)}
403
+ disabled={isToggling}
404
+ title={isRunning ? 'Stop' : 'Start'}
405
+ className={`w-7 h-7 rounded-[6px] flex items-center justify-center transition-all cursor-pointer border-none ${
406
+ isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
407
+ } ${isRunning
408
+ ? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
409
+ : 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
410
+ } disabled:opacity-50`}
411
+ >
412
+ {isToggling ? (
413
+ <span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
414
+ ) : isRunning ? (
415
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>
416
+ ) : (
417
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 21,12 6,21" /></svg>
418
+ )}
419
+ </button>
420
+ )}
421
+ </div>
422
+ </div>
423
+ </div>
424
+ )
425
+ })}
287
426
  </div>
288
- </div>
289
- </div>
290
- )
291
- })}
427
+ </section>
428
+ )
429
+ })}
292
430
  </div>
293
431
  </div>
294
432
  )